// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // app_security.cpp — Wallet encryption, lock screen, PIN management // Split from app.cpp for maintainability. #include "app.h" #include "rpc/rpc_client.h" #include "rpc/rpc_worker.h" #include "rpc/connection.h" #include "services/wallet_security_workflow_executor.h" #include "config/settings.h" #include "daemon/embedded_daemon.h" #include "ui/notifications.h" #include "ui/material/color_theme.h" #include "ui/material/type.h" #include "ui/material/typography.h" #include "ui/material/draw_helpers.h" #include "ui/schema/ui_schema.h" #include "ui/theme.h" #include "ui/effects/imgui_acrylic.h" #include "ui/windows/mining_tab.h" #include "util/platform.h" #include "util/secure_vault.h" #include "util/perf_log.h" #include "embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include #include #include #include #include namespace dragonx { using json = nlohmann::json; namespace { class WalletSecurityRpcAdapter : public services::WalletSecurityController::RpcGateway { public: explicit WalletSecurityRpcAdapter(rpc::RPCClient* rpc, std::string source = "Security settings") : rpc_(rpc), source_(std::move(source)) {} bool encryptWallet(const std::string& passphrase, std::string& error) override { return callWithError([&] { rpc_->call("encryptwallet", {passphrase}); }, error); } bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) override { return callWithError([&] { rpc_->call("walletpassphrase", {passphrase, timeoutSeconds}); }, error); } bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) override { return callWithError([&] { rpc_->call("z_exportwallet", {fileName}, timeoutSeconds); }, error); } bool importWallet(const std::string& filePath, long timeoutSeconds, std::string& error) override { return callWithError([&] { rpc_->call("z_importwallet", {filePath}, timeoutSeconds); }, error); } private: template bool callWithError(Fn&& fn, std::string& error) { if (!rpc_) { error = "RPC client unavailable"; return false; } try { rpc::RPCClient::TraceScope trace(source_); fn(); return true; } catch (const std::exception& e) { error = e.what(); return false; } } rpc::RPCClient* rpc_ = nullptr; std::string source_; }; class WalletSecurityVaultAdapter : public services::WalletSecurityController::VaultGateway { public: explicit WalletSecurityVaultAdapter(util::SecureVault* vault) : vault_(vault) {} bool storePin(const std::string& pin, const std::string& passphrase) override { return vault_ && vault_->store(pin, passphrase); } private: util::SecureVault* vault_ = nullptr; }; class WalletSecurityDecryptRpcAdapter : public services::WalletSecurityWorkflowExecutor::RpcGateway { public: using StopFn = std::function; WalletSecurityDecryptRpcAdapter(rpc::RPCClient* rpc, StopFn stopFn, std::string source = "Security / Decrypt wallet workflow") : rpc_(rpc), stopFn_(std::move(stopFn)), source_(std::move(source)) {} bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) override { return callWithError([&] { rpc_->call("walletpassphrase", {passphrase, timeoutSeconds}); }, error); } bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) override { return callWithError([&] { rpc_->call("z_exportwallet", {fileName}, timeoutSeconds); }, error); } bool requestDaemonStop(std::string& error) override { if (!rpc_) { error = "RPC client unavailable"; return false; } bool ok = stopFn_ ? stopFn_(*rpc_, "Decrypt export daemon stop") : false; if (!ok) error = "Stop RPC failed"; return ok; } bool probeDaemon(std::string& error) override { return callWithError([&] { rpc_->call("getinfo"); }, error); } private: template bool callWithError(Fn&& fn, std::string& error) { if (!rpc_) { error = "RPC client unavailable"; return false; } try { rpc::RPCClient::TraceScope trace(source_); fn(); return true; } catch (const std::exception& e) { error = e.what(); return false; } } rpc::RPCClient* rpc_ = nullptr; StopFn stopFn_; std::string source_; }; class WalletSecurityImportRpcAdapter : public services::WalletSecurityWorkflowExecutor::ImportGateway { public: WalletSecurityImportRpcAdapter(rpc::RPCClient* fallbackRpc, rpc::ConnectionConfig config) : fallbackRpc_(fallbackRpc), config_(std::move(config)) {} bool importWallet(const std::string& exportPath, long timeoutSeconds, std::string& error) override { auto importRpc = std::make_unique(); bool importRpcOk = importRpc->connect(config_.host, config_.port, config_.rpcuser, config_.rpcpassword, config_.use_tls); if (!importRpcOk) importRpc.reset(); auto* rpcForImport = importRpc ? importRpc.get() : fallbackRpc_; if (!rpcForImport) { error = "RPC client unavailable"; return false; } try { rpc::RPCClient::TraceScope trace("Security / Import wallet workflow"); rpcForImport->call("z_importwallet", {exportPath}, timeoutSeconds); if (importRpc) importRpc->disconnect(); return true; } catch (const std::exception& e) { if (importRpc) importRpc->disconnect(); error = e.what(); return false; } } private: rpc::RPCClient* fallbackRpc_ = nullptr; rpc::ConnectionConfig config_; }; class WalletSecurityFileAdapter : public services::WalletSecurityWorkflowExecutor::FileGateway { public: std::string dataDir() override { return util::Platform::getDragonXDataDir(); } bool backupEncryptedWallet(const services::WalletSecurityWorkflowExecutor::WalletFilePlan& filePlan, std::string& error) override { std::error_code ec; if (!std::filesystem::exists(filePlan.walletPath, ec)) return true; std::filesystem::remove(filePlan.backupPath, ec); ec.clear(); std::filesystem::rename(filePlan.walletPath, filePlan.backupPath, ec); if (ec) { error = ec.message(); return false; } return true; } }; class WalletSecurityDaemonAdapter : public services::WalletSecurityWorkflowExecutor::DaemonGateway { public: WalletSecurityDaemonAdapter(App& app, const util::AsyncTaskManager::Token& token) : app_(app), token_(token) {} bool isUsingEmbeddedDaemon() const override { return app_.isUsingEmbeddedDaemon(); } void stopEmbeddedDaemon() override { app_.stopEmbeddedDaemon(); } bool startEmbeddedDaemon() override { return app_.startEmbeddedDaemon(); } bool cancelled() const override { return token_.cancelled(); } bool shuttingDown() const override { return app_.isShuttingDown(); } void sleepForMs(int milliseconds) override { std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds))); } private: App& app_; const util::AsyncTaskManager::Token& token_; }; } // namespace // =========================================================================== // Wallet encryption helpers // =========================================================================== void App::encryptWalletWithPassphrase(const std::string& passphrase) { if (!rpc_ || !rpc_->isConnected()) return; encrypt_in_progress_ = true; encrypt_status_ = "Encrypting wallet..."; if (worker_) { worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb { WalletSecurityRpcAdapter rpcAdapter(rpc_.get()); auto result = wallet_security_.runDeferredEncryption( {std::move(passphrase), {}}, rpcAdapter, nullptr); if (result.encrypted) { return [this]() { encrypt_in_progress_ = false; encrypt_status_ = "Wallet encrypted. Restarting daemon..."; DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n"); // Immediately update local encryption state so the // settings page reflects that the wallet is now encrypted // (the daemon is about to restart, so getwalletinfo won't // be available for a while). state_.encrypted = true; state_.locked = true; state_.unlocked_until = 0; state_.encryption_state_known = true; // Transition settings dialog to PIN setup phase if (show_encrypt_dialog_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { encrypt_dialog_phase_ = EncryptDialogPhase::PinSetup; } ui::Notifications::instance().info( "Wallet encrypted successfully", 5.0f); // The daemon shuts itself down after encryptwallet. // Update connection_status_ so the loading overlay // explains why the daemon is restarting. if (isUsingEmbeddedDaemon()) { connection_status_ = TR("restarting_after_encryption"); // Give daemon a moment to shut down, then restart // (do this off the main thread to avoid stalling the UI) async_tasks_.submit("encrypt-daemon-restart", [this](const util::AsyncTaskManager::Token& token) { for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (token.cancelled() || shutting_down_) return; stopEmbeddedDaemon(); if (token.cancelled() || shutting_down_) return; startEmbeddedDaemon(); // tryConnect will be called by the update loop }); } else { ui::Notifications::instance().warning( "Please restart your daemon for encryption to take effect."); } }; } else { std::string err = result.error; return [this, err]() { encrypt_in_progress_ = false; encrypt_status_ = "Encryption failed: " + err; DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str()); ui::Notifications::instance().error( "Encryption failed: " + err); // Return to passphrase entry on failure if (show_encrypt_dialog_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; } }; } }); } } // --------------------------------------------------------------------------- // Deferred encryption — runs after wizard exits, encrypts wallet in background // Called every frame from render() until the task completes. // --------------------------------------------------------------------------- void App::processDeferredEncryption() { if (!wallet_security_.hasDeferredEncryption()) return; // Phase 1: wait for daemon connection if (!state_.connected || !rpc_ || !rpc_->isConnected()) { if (wallet_security_.shouldAttemptDeferredConnect(ImGui::GetTime())) { if (!connection_in_progress_) { // Just try to connect — tryConnect is now async tryConnect(); if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { startEmbeddedDaemon(); } } } return; // try again next frame } // Phase 2: connected — launch encryption if (!encrypt_in_progress_) { auto deferredEncryption = wallet_security_.deferredEncryption(); std::string passphrase = std::move(deferredEncryption.passphrase); std::string pin = std::move(deferredEncryption.pin); encrypt_in_progress_ = true; encrypt_status_ = "Encrypting wallet..."; if (worker_) { worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb { WalletSecurityRpcAdapter rpcAdapter(rpc_.get()); WalletSecurityVaultAdapter vaultAdapter(vault_.get()); auto result = wallet_security_.runDeferredEncryption( std::move(request), rpcAdapter, vault_ ? &vaultAdapter : nullptr); if (result.encrypted) { return [this, result]() { encrypt_in_progress_ = false; encrypt_status_.clear(); DEBUG_LOGF("[App] Wallet encrypted (deferred)\n"); // Finalize PIN settings on main thread if (result.pinProvided) { if (result.pinStored) { settings_->setPinEnabled(true); settings_->save(); ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f); } else { ui::Notifications::instance().warning( "Wallet encrypted but PIN vault failed"); } } else { ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f); } wallet_security_.clearDeferredEncryption(); // Restart daemon (it shuts itself down after encryptwallet) if (isUsingEmbeddedDaemon()) { async_tasks_.submit("deferred-encrypt-daemon-restart", [this](const util::AsyncTaskManager::Token& token) { for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (token.cancelled() || shutting_down_) return; stopEmbeddedDaemon(); if (token.cancelled() || shutting_down_) return; startEmbeddedDaemon(); // tryConnect will be called by the update loop }); } else { ui::Notifications::instance().warning( "Please restart your daemon for encryption to take effect."); } }; } else { std::string err = result.error; return [this, err]() { encrypt_in_progress_ = false; encrypt_status_ = "Encryption failed: " + err; DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str()); ui::Notifications::instance().error("Encryption failed: " + err); wallet_security_.clearDeferredEncryption(); }; } }); } } } void App::unlockWallet(const std::string& passphrase, int timeout) { if (!rpc_ || !rpc_->isConnected() || !worker_) return; lock_unlock_in_progress_ = true; // Use fast-lane worker to bypass head-of-line blocking behind refreshData. auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); w->post([this, r, passphrase = std::string(passphrase), timeout]() mutable -> rpc::RPCWorker::MainCb { std::string err_msg; WalletSecurityRpcAdapter rpcAdapter(r); bool ok = rpcAdapter.unlockWallet(passphrase, timeout, err_msg); std::string cachePassphrase = passphrase; util::SecureVault::secureZero(passphrase.data(), passphrase.size()); return [this, ok, err_msg, timeout, passphrase = std::move(cachePassphrase)]() mutable { lock_unlock_in_progress_ = false; if (ok) { lock_error_msg_.clear(); lock_attempts_ = 0; memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_)); last_interaction_ = std::chrono::steady_clock::now(); // Set unlock state immediately — walletpassphrase // already succeeded, no need for another RPC round-trip. state_.encrypted = true; state_.locked = false; state_.unlocked_until = std::time(nullptr) + timeout; unlockTransactionHistoryCacheWithPassphrase(passphrase); } else { lock_attempts_++; lock_error_msg_ = TR("incorrect_passphrase"); lock_error_timer_ = 3.0f; memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_)); float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f); int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f); if (lock_attempts_ >= maxAttempts) { lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8)); } DEBUG_LOGF("[App] Wallet unlock failed (attempt %d): %s\n", lock_attempts_, err_msg.c_str()); } util::SecureVault::secureZero(passphrase.data(), passphrase.size()); }; }); } void App::lockWallet() { if (!rpc_ || !rpc_->isConnected() || !worker_) return; if (lock_unlock_in_progress_) return; // Prevent duplicate async calls lock_unlock_in_progress_ = true; // Use fast-lane worker to avoid blocking behind refreshData. auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); w->post([this, r]() -> rpc::RPCWorker::MainCb { bool ok = false; try { r->call("walletlock"); ok = true; } catch (...) {} return [this, ok]() { lock_unlock_in_progress_ = false; if (ok) { state_.locked = true; state_.unlocked_until = 0; resetTransactionHistoryCacheSession(); DEBUG_LOGF("[App] Wallet locked\n"); } }; }); } void App::changePassphrase(const std::string& oldPass, const std::string& newPass) { if (!rpc_ || !rpc_->isConnected() || !worker_) return; encrypt_in_progress_ = true; encrypt_status_ = "Changing passphrase..."; auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); w->post([this, r, oldPass = std::string(oldPass), newPass = std::string(newPass)]() mutable -> rpc::RPCWorker::MainCb { bool ok = false; std::string err_msg; try { r->call("walletpassphrasechange", {oldPass, newPass}); ok = true; } catch (const std::exception& e) { err_msg = e.what(); } std::string cacheNewPass = newPass; util::SecureVault::secureZero(oldPass.data(), oldPass.size()); util::SecureVault::secureZero(newPass.data(), newPass.size()); return [this, ok, err_msg, newPass = std::move(cacheNewPass)]() mutable { encrypt_in_progress_ = false; if (ok) { encrypt_status_.clear(); show_change_passphrase_ = false; memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_)); memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_)); memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_)); unlockTransactionHistoryCacheWithPassphrase(newPass); storeTransactionHistoryCacheIfAvailable(); ui::Notifications::instance().info("Passphrase changed successfully"); } else { encrypt_status_ = "Failed: " + err_msg; } util::SecureVault::secureZero(newPass.data(), newPass.size()); }; }); } // =========================================================================== // Refresh wallet encryption state (from getwalletinfo) // =========================================================================== void App::refreshWalletEncryptionState() { if (!rpc_ || !rpc_->isConnected() || !worker_) return; worker_->post([this]() -> rpc::RPCWorker::MainCb { json result; bool ok = false; try { rpc::RPCClient::TraceScope trace("Security / Wallet encryption state"); result = rpc_->call("getwalletinfo"); ok = true; } catch (...) {} if (!ok) return nullptr; return [this, result]() { try { if (result.contains("unlocked_until")) { state_.encrypted = true; int64_t until = result["unlocked_until"].get(); state_.unlocked_until = until; state_.locked = (until == 0); state_.encryption_state_known = true; if (state_.locked) { resetTransactionHistoryCacheSession(); } else if (state_.transactions.empty()) { loadTransactionHistoryCacheIfAvailable(); } else { storeTransactionHistoryCacheIfAvailable(); } } else { state_.encrypted = false; state_.locked = false; state_.unlocked_until = 0; state_.encryption_state_known = true; if (state_.transactions.empty()) { loadTransactionHistoryCacheIfAvailable(); } else { storeTransactionHistoryCacheIfAvailable(); } // Wallet is no longer encrypted — if a PIN vault exists, // it's stale (passphrase it protects is gone). Reset PIN // as if it were never set. if (vault_ && vault_->hasVault()) { DEBUG_LOGF("[App] Wallet unencrypted but PIN vault exists — removing stale vault\n"); vault_->removeVault(); } if (settings_ && settings_->getPinEnabled()) { settings_->setPinEnabled(false); settings_->save(); } } } catch (...) {} }; }); } // =========================================================================== // Auto-lock on idle // =========================================================================== void App::checkAutoLock() { if (!state_.isEncrypted() || state_.isLocked()) return; // Don't auto-lock while mining — mining is a long-running intentional // operation and locking the wallet on the daemon side stops solo mining. bool miningActive = state_.mining.generate || (xmrig_manager_ && xmrig_manager_->isRunning()); if (miningActive) { // Keep resetting the idle timer so we don't lock the instant mining stops last_interaction_ = std::chrono::steady_clock::now(); return; } int timeout = settings_ ? settings_->getAutoLockTimeout() : 300; if (timeout <= 0) return; // disabled auto now = std::chrono::steady_clock::now(); float elapsed = std::chrono::duration(now - last_interaction_).count(); if (elapsed >= (float)timeout) { lockWallet(); DEBUG_LOGF("[App] Auto-locked wallet after %d seconds idle\n", timeout); } } // =========================================================================== // Mine when idle — auto-start/stop mining based on system idle state // Supports two modes: // 1. Start/Stop mode (default): start mining when idle, stop when active // 2. Thread scaling mode: mining stays running, thread count changes // =========================================================================== void App::checkIdleMining() { if (!settings_ || !settings_->getMineWhenIdle()) { // Feature disabled — if we previously auto-started, stop now if (idle_mining_active_) { idle_mining_active_ = false; idle_scaled_to_idle_ = false; if (settings_ && settings_->getPoolMode()) { if (xmrig_manager_ && xmrig_manager_->isRunning()) stopPoolMining(); } else { if (state_.mining.generate) stopMining(); } } // Reset scaling state when feature is off if (idle_scaled_to_idle_) idle_scaled_to_idle_ = false; return; } // Skip idle mining adjustments while thread benchmark is running if (ui::IsMiningBenchmarkActive()) return; int idleSec = util::Platform::getSystemIdleSeconds(); int delay = settings_->getMineIdleDelay(); bool isPool = settings_->getPoolMode(); bool threadScaling = settings_->getIdleThreadScaling(); int maxThreads = std::max(1, (int)std::thread::hardware_concurrency()); // GPU-aware idle detection: if enabled, treat GPU utilization >= 10% // as "user active" (e.g. watching a video). Disabled = unrestricted // mode that only looks at keyboard/mouse input. bool gpuBusy = false; if (settings_->getIdleGpuAware()) { int gpuUtil = util::Platform::getGpuUtilization(); gpuBusy = (gpuUtil >= 10); } bool systemIdle = (idleSec >= delay) && !gpuBusy; // Check if mining is already running (manually started by user) bool miningActive = isPool ? (xmrig_manager_ && xmrig_manager_->isRunning()) : state_.mining.generate; if (threadScaling) { // --- Thread scaling mode --- // Mining must already be running (started by user). We just adjust threads. if (!miningActive || mining_toggle_in_progress_.load()) return; int activeThreads = settings_->getIdleThreadsActive(); int idleThreads = settings_->getIdleThreadsIdle(); // Resolve auto values: active defaults to half, idle defaults to all if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2); if (idleThreads <= 0) idleThreads = maxThreads; if (systemIdle) { // System is idle — scale up to idle thread count if (!idle_scaled_to_idle_) { idle_scaled_to_idle_ = true; if (isPool) { stopPoolMining(); startPoolMining(idleThreads); } else { startMining(idleThreads); } DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads); } } else { // User is active (or GPU busy) — scale down to active thread count if (idle_scaled_to_idle_) { idle_scaled_to_idle_ = false; if (isPool) { stopPoolMining(); startPoolMining(activeThreads); } else { startMining(activeThreads); } DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads); } else { // Mining just started while user is active — ensure active // thread count is applied (grid selection may differ). int currentThreads = isPool ? xmrig_manager_->getStats().threads_active : state_.mining.genproclimit; if (currentThreads > 0 && currentThreads != activeThreads) { if (isPool) { stopPoolMining(); startPoolMining(activeThreads); } else { startMining(activeThreads); } DEBUG_LOGF("[App] Idle thread scaling: initial %d -> %d threads (active)\n", currentThreads, activeThreads); } } } } else { // --- Start/Stop mode (original behavior) --- if (systemIdle) { // System is idle — start mining if not already running if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) { // For solo mining, need daemon connected and synced if (!isPool && (!state_.connected || state_.sync.syncing)) return; int threads = settings_->getPoolThreads(); if (threads <= 0) threads = std::max(1, maxThreads / 2); idle_mining_active_ = true; if (isPool) startPoolMining(threads); else startMining(threads); DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec); } } else { // User is active — stop mining if we auto-started it if (idle_mining_active_) { idle_mining_active_ = false; if (isPool) { if (xmrig_manager_ && xmrig_manager_->isRunning()) stopPoolMining(); } else { if (state_.mining.generate) stopMining(); } DEBUG_LOGF("[App] Idle mining stopped — user returned\n"); } } } } // =========================================================================== // Restart Setup Wizard (from Settings) // =========================================================================== // =========================================================================== // Lock Screen Rendering // =========================================================================== void App::renderLockScreen() { using namespace ui::material; ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 winPos = ImGui::GetWindowPos(); ImVec2 winSize = ImGui::GetWindowSize(); // Optional backdrop (0 = no darkening) float backdropAlpha = ui::schema::UI().drawElement("screens.lock-screen", "backdrop-alpha").opacity; if (backdropAlpha > 0.0f) { ImU32 backdropCol = IM_COL32(0, 0, 0, (int)(255 * backdropAlpha)); dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), backdropCol); } // Card const auto& S = ui::schema::UI(); float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f); float cardH = S.drawElement("screens.lock-screen", "card").height; if (cardW <= 0) cardW = 400.0f; if (cardH <= 0) cardH = 320.0f; float cardX = winPos.x + (winSize.x - cardW) * 0.5f; float cardY = winPos.y + (winSize.y - cardH) * 0.5f; ImVec2 cardMin(cardX, cardY); ImVec2 cardMax(cardX + cardW, cardY + cardH); ImU32 cardBg = ui::material::SurfaceVariant(); dl->AddRectFilled(cardMin, cardMax, cardBg, 16.0f); float cy = cardY + 24.0f; // Logo float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f); if (logo_tex_ != 0) { float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f; float logoW = logoSize * aspect; float logoX = cardX + (cardW - logoW) * 0.5f; dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize)); } cy += logoSize + 16.0f; // Title ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font); if (!titleFont) titleFont = ui::material::Type().h5(); ImFont* captionFont = S.resolveFont(S.label("screens.lock-screen", "error-text").font); if (!captionFont) captionFont = ui::material::Type().caption(); ImU32 textCol = ui::material::OnSurface(); { const char* title = "Wallet Locked"; ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title); dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title); cy += ts.y + 20.0f; } // Lockout timer if (lock_lockout_timer_ > 0.0f) { lock_lockout_timer_ -= ImGui::GetIO().DeltaTime; if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0; char msg[128]; snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg); cy += captionFont->LegacySize + 12.0f; } // Check if PIN vault is available bool hasPinVault = vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled(); // Mode toggle (PIN / Passphrase) — only show if PIN vault exists if (hasPinVault) { const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD; const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase"; const char* switchLabel = lock_use_pin_ ? "Use passphrase instead" : "Use PIN instead"; // Current mode indicator — icon with icon font, text with caption font ImFont* iconFont = ui::material::Type().iconSmall(); ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, modeIcon); ImVec2 textSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, modeText); float totalW = iconSize.x + textSize.x; float startX = cardX + (cardW - totalW) * 0.5f; float textY = cy + (iconSize.y - textSize.y) * 0.5f; // vertically align text to icon dl->AddText(iconFont, iconFont->LegacySize, ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText); cy += std::max(iconSize.y, textSize.y) + 8.0f; // Switch link ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel); float switchX = cardX + (cardW - sls.x) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(switchX, cy)); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); if (ImGui::InvisibleButton("##switch_mode", sls)) { lock_use_pin_ = !lock_use_pin_; memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_)); memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_)); lock_error_msg_.clear(); lock_screen_was_visible_ = false; // re-trigger auto-focus for new input } dl->AddText(captionFont, captionFont->LegacySize, ImVec2(switchX, cy), ui::material::Primary(), switchLabel); ImGui::PopStyleColor(); cy += captionFont->LegacySize + 12.0f; } else { // No PIN vault — don't show toggle, force passphrase mode lock_use_pin_ = false; } // Input field float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f); if (inputW <= 0) inputW = 320.0f; float inputX = cardX + (cardW - inputW) * 0.5f; bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_; bool submitted = false; if (lock_use_pin_ && hasPinVault) { // PIN input ImGui::SetCursorScreenPos(ImVec2(inputX, cy)); ImGui::PushItemWidth(inputW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal; if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue; submitted = ImGui::InputText("##lock_pin", lock_pin_buf_, sizeof(lock_pin_buf_), pinFlags); ImGui::PopStyleVar(); ImGui::PopItemWidth(); } else { // Passphrase input (original) ImGui::SetCursorScreenPos(ImVec2(inputX, cy)); ImGui::PushItemWidth(inputW); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password; if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue; submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_, sizeof(lock_passphrase_buf_), inputFlags); ImGui::PopStyleVar(); ImGui::PopItemWidth(); } cy += 40.0f + 12.0f; // Focus the input when the lock screen first appears. // IsWindowAppearing() does not work here because the lock screen is // rendered inside ##ContentArea which has already been alive since the // first frame. Instead we track the hidden→visible transition ourselves. if (!lock_screen_was_visible_) { ImGui::SetKeyboardFocusHere(-1); lock_screen_was_visible_ = true; } // Error message if (!lock_error_msg_.empty() && lock_error_timer_ > 0) { lock_error_timer_ -= ImGui::GetIO().DeltaTime; ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str()); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(), lock_error_msg_.c_str()); cy += captionFont->LegacySize + 8.0f; } // "Unlocking..." feedback while worker thread is running // Always reserve the vertical space so the button doesn't shift. { float rowH = captionFont->LegacySize + 8.0f; if (lock_unlock_in_progress_) { // Animated spinner dots int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4; const char* dotStr[] = {"", ".", "..", "..."}; char msg[64]; snprintf(msg, sizeof(msg), "Unlocking%s", dotStr[dots]); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Primary(), msg); } cy += rowH; } // Unlock button float unlockW = S.drawElement("screens.lock-screen", "unlock-button").getFloat("width", 320.0f); float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height; if (unlockW <= 0) unlockW = 320.0f; if (unlockH <= 0) unlockH = 44.0f; float unlockX = cardX + (cardW - unlockW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(unlockX, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f); ImGui::BeginDisabled(!canSubmit); bool btnClicked = ImGui::Button("Unlock", ImVec2(unlockW, unlockH)); ImGui::EndDisabled(); ImGui::PopStyleVar(); ImGui::PopStyleColor(3); // Handle submit (Enter key or button click) if ((submitted || btnClicked) && canSubmit) { // Derive daemon unlock timeout from auto-lock setting: // 2x the idle timeout so the daemon stays unlocked longer than // the GUI auto-lock, acting as a safety net if the GUI crashes. // Floor of 600s (10 min) when auto-lock is off or very short. int autoLock = settings_ ? settings_->getAutoLockTimeout() : 300; int timeout = (autoLock > 0) ? std::max(600, autoLock * 2) : 86400; if (lock_use_pin_ && hasPinVault && strlen(lock_pin_buf_) > 0) { // PIN unlock — run Argon2id key derivation + RPC off the main // thread so the UI stays responsive instead of freezing. std::string pin(lock_pin_buf_); memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_)); lock_unlock_in_progress_ = true; // Use fast-lane worker for priority unlock. auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); if (w) { w->post([this, r, pin, timeout]() -> rpc::RPCWorker::MainCb { // Heavy Argon2id derivation runs here (worker thread) std::string passphrase; bool vaultOk = vault_ && vault_->retrieve(pin, passphrase); if (!vaultOk) { bool noVault = !vault_ || !vault_->hasVault(); return [this, noVault]() { lock_unlock_in_progress_ = false; if (noVault) { // Vault file missing — switch to passphrase mode lock_error_msg_ = TR("pin_not_set"); lock_use_pin_ = false; } else { lock_attempts_++; lock_error_msg_ = TR("incorrect_pin"); } lock_error_timer_ = 3.0f; float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f); int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f); if (lock_attempts_ >= maxAttempts) { lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8)); } }; } // Vault decrypted — now unlock wallet via RPC (also on worker thread) bool rpcOk = false; std::string rpcErr; try { if (r && r->isConnected()) { r->call("walletpassphrase", {passphrase, timeout}); rpcOk = true; } else { rpcErr = "Not connected to daemon"; } } catch (const std::exception& e) { rpcErr = e.what(); } if (rpcOk) { return [this, timeout, passphrase = std::move(passphrase)]() mutable { lock_unlock_in_progress_ = false; lock_error_msg_.clear(); lock_attempts_ = 0; memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_)); last_interaction_ = std::chrono::steady_clock::now(); // Set unlock state immediately — walletpassphrase // already succeeded, no need for another RPC round-trip. state_.encrypted = true; state_.locked = false; state_.unlocked_until = std::time(nullptr) + timeout; unlockTransactionHistoryCacheWithPassphrase(passphrase); util::SecureVault::secureZero(passphrase.data(), passphrase.size()); }; } else { return [this, rpcErr, passphrase = std::move(passphrase)]() mutable { lock_unlock_in_progress_ = false; lock_attempts_++; lock_error_msg_ = "Unlock failed: " + rpcErr; lock_error_timer_ = 3.0f; util::SecureVault::secureZero(passphrase.data(), passphrase.size()); }; } }); } } else if (strlen(lock_passphrase_buf_) > 0) { // Direct passphrase unlock unlockWallet(std::string(lock_passphrase_buf_), timeout); } } } // =========================================================================== // Encrypt Wallet Dialog (post-first-run, from Settings) // =========================================================================== void App::renderEncryptWalletDialog() { if (!show_encrypt_dialog_ && !show_change_passphrase_) return; using namespace ui::material; // Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup if (show_encrypt_dialog_) { const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) ? "Quick-Unlock PIN" : "Encrypt Wallet"; // Prevent closing via X button while encrypting bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting); bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr; if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) { // ---- Phase 1: Passphrase entry ---- if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1)); ImGui::TextWrapped(ICON_MD_WARNING " If you lose your passphrase, you lose access to your funds."); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::TextWrapped("Encrypting your wallet protects your private keys " "with a passphrase. After encryption, the daemon will restart."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("Passphrase:"); ImGui::PushItemWidth(-1); ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); ImGui::Text("Confirm:"); ImGui::PushItemWidth(-1); ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); // Strength meter bar { size_t len = strlen(encrypt_pass_buf_); const char* strengthLabel = "Weak"; ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f); float strengthPct = 0.25f; if (len >= 16) { strengthLabel = "Strong"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; } else if (len >= 12) { strengthLabel = "Good"; strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; } else if (len >= 8) { strengthLabel = "Fair"; strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; } float barW = ImGui::GetContentRegionAvail().x; float barH = 4.0f; ImVec2 p = ImGui::GetCursorScreenPos(); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH), IM_COL32(255,255,255,30), 2.0f); if (len > 0) dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH), ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f); ImGui::Dummy(ImVec2(barW, barH)); ImGui::Text("Strength: %s", strengthLabel); } if (!encrypt_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str()); } ImGui::Spacing(); bool valid = strlen(encrypt_pass_buf_) >= 8 && strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; ImGui::BeginDisabled(!valid || encrypt_in_progress_); if (ImGui::Button("Encrypt Wallet", ImVec2(btnW, 40))) { std::string pass(encrypt_pass_buf_); enc_dlg_saved_passphrase_ = pass; memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_)); memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_)); encrypt_dialog_phase_ = EncryptDialogPhase::Encrypting; encryptWalletWithPassphrase(pass); util::SecureVault::secureZero(&pass[0], pass.size()); } ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(btnW, 40))) { memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_)); memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_)); show_encrypt_dialog_ = false; } // ---- Phase 2: Encrypting in progress ---- } else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { const char* statusTitle = encrypt_in_progress_ ? "Encrypting wallet..." : encrypt_status_.c_str(); ImGui::Text("%s", statusTitle); ImGui::Spacing(); // Indeterminate progress bar { float barW = ImGui::GetContentRegionAvail().x; float barH = 6.0f; ImVec2 p = ImGui::GetCursorScreenPos(); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH), IM_COL32(255,255,255,25), 3.0f); float t = (float)ImGui::GetTime(); float pulse = 0.5f + 0.5f * sinf(t * 2.0f); float segW = barW * 0.3f; float segX = p.x + (barW - segW) * pulse; dl->AddRectFilled(ImVec2(segX, p.y), ImVec2(segX + segW, p.y + barH), ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]), 3.0f); ImGui::Dummy(ImVec2(barW, barH)); } if (!encrypt_status_.empty()) { ImGui::TextColored(ImVec4(1, 1, 1, 0.6f), "%s", encrypt_status_.c_str()); } ImGui::Spacing(); ImGui::TextColored(ImVec4(1,1,1,0.4f), "Please wait, do not close the application."); // Transition to PIN phase when encryption finishes successfully if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) { // encryptWalletWithPassphrase callback handles the transition } // ---- Phase 3: PIN setup (after successful encryption) ---- } else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1)); ImGui::Text(ICON_MD_CHECK_CIRCLE " Wallet encrypted successfully!"); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::TextWrapped("A 4-8 digit PIN lets you unlock your wallet " "without typing the full passphrase every time."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("PIN (4-8 digits):"); ImGui::PushItemWidth(-1); ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); ImGui::Text("Confirm PIN:"); ImGui::PushItemWidth(-1); ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_, sizeof(enc_dlg_pin_confirm_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); if (!enc_dlg_pin_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", enc_dlg_pin_status_.c_str()); } ImGui::Spacing(); std::string pinStr(enc_dlg_pin_buf_); std::string pinConfirm(enc_dlg_pin_confirm_buf_); bool pinValid = util::SecureVault::isValidPin(pinStr) && pinStr == pinConfirm; bool hasPassphrase = !enc_dlg_saved_passphrase_.empty(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_); if (ImGui::Button("Set PIN", ImVec2(btnW, 40))) { pin_in_progress_ = true; enc_dlg_pin_status_.clear(); std::string savedPass = enc_dlg_saved_passphrase_; if (worker_ && vault_) { worker_->post([this, pinStr, savedPass]() -> rpc::RPCWorker::MainCb { // Argon2id runs here (worker thread) bool ok = vault_->store(pinStr, savedPass); return [this, ok]() { if (ok) { settings_->setPinEnabled(true); settings_->save(); pin_in_progress_ = false; ui::Notifications::instance().info("PIN set successfully"); // Clean up if (!enc_dlg_saved_passphrase_.empty()) { util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0], enc_dlg_saved_passphrase_.size()); enc_dlg_saved_passphrase_.clear(); } memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_)); memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_)); show_encrypt_dialog_ = false; } else { enc_dlg_pin_status_ = "Failed to create PIN vault"; pin_in_progress_ = false; } }; }); } else { enc_dlg_pin_status_ = "Failed to create PIN vault"; pin_in_progress_ = false; } } ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Skip", ImVec2(btnW, 40))) { if (!enc_dlg_saved_passphrase_.empty()) { util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0], enc_dlg_saved_passphrase_.size()); enc_dlg_saved_passphrase_.clear(); } memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_)); memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_)); show_encrypt_dialog_ = false; ui::Notifications::instance().info( "PIN skipped. You can set one later in Settings."); } } EndOverlayDialog(); } // Clean up saved passphrase if dialog was closed via X button if (!show_encrypt_dialog_ && !enc_dlg_saved_passphrase_.empty()) { util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0], enc_dlg_saved_passphrase_.size()); enc_dlg_saved_passphrase_.clear(); } } // Change passphrase dialog if (show_change_passphrase_) { if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) { ImGui::Text("Current Passphrase:"); ImGui::PushItemWidth(-1); ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); ImGui::Text("New Passphrase:"); ImGui::PushItemWidth(-1); ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); ImGui::Text("Confirm New:"); ImGui::PushItemWidth(-1); ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); if (!encrypt_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str()); } ImGui::Spacing(); bool valid = strlen(change_old_pass_buf_) > 0 && strlen(change_new_pass_buf_) >= 8 && strcmp(change_new_pass_buf_, change_confirm_buf_) == 0; ImGui::BeginDisabled(!valid || encrypt_in_progress_); if (ImGui::Button("Change Passphrase", ImVec2(-1, 40))) { changePassphrase(std::string(change_old_pass_buf_), std::string(change_new_pass_buf_)); } ImGui::EndDisabled(); EndOverlayDialog(); } } } // =========================================================================== // Decrypt (Remove Encryption) Dialog // =========================================================================== // Flow: // Phase 0: Enter current passphrase // Phase 1: Working — unlock → export → stop → rename → restart → import // Phase 2: Success // Phase 3: Error // =========================================================================== void App::renderDecryptWalletDialog() { if (!show_decrypt_dialog_) return; using namespace ui::material; using DecryptPhase = services::WalletSecurityWorkflow::DecryptPhase; using DecryptStep = services::WalletSecurityWorkflow::DecryptStep; auto decryptState = wallet_security_workflow_.snapshot(); bool canClose = wallet_security_workflow_.canClose(); bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr; if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) { // ---- Phase 0: Passphrase entry ---- if (decryptState.phase == DecryptPhase::PassphraseEntry) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1)); ImGui::TextWrapped(ICON_MD_WARNING " This will remove encryption from your wallet. " "Your private keys will be stored unprotected on disk."); ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::TextWrapped( "The wallet will be exported, the daemon restarted with a fresh " "unencrypted wallet, and all keys re-imported. This may take " "several minutes depending on wallet size."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("Current Passphrase:"); ImGui::PushItemWidth(-1); bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_, sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue); ImGui::PopItemWidth(); if (!decryptState.status.empty()) { ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decryptState.status.c_str()); } ImGui::Spacing(); bool valid = strlen(decrypt_pass_buf_) >= 1; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; ImGui::BeginDisabled(!valid || decryptState.inProgress); if (ImGui::Button("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) { std::string passphrase(decrypt_pass_buf_); memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_)); wallet_security_workflow_.start(std::chrono::steady_clock::now()); // Run entire decrypt flow on worker thread if (worker_) { worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb { WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(), [this](rpc::RPCClient& client, const char* context) { return sendStopCommandSafely(client, context); }); auto unlock = services::WalletSecurityWorkflowExecutor::unlockWallet(passphrase, decryptRpc); if (!unlock.ok) { return [this]() { wallet_security_workflow_.failEntry("Incorrect passphrase"); }; } // Update step on main thread return [this]() { wallet_security_workflow_.advanceTo(DecryptStep::ExportKeys, services::WalletSecurityWorkflow::stepStatus(DecryptStep::ExportKeys), std::chrono::steady_clock::now()); // Continue with step 2 worker_->post([this]() -> rpc::RPCWorker::MainCb { WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(), [this](rpc::RPCClient& client, const char* context) { return sendStopCommandSafely(client, context); }); WalletSecurityFileAdapter files; auto exportOutcome = services::WalletSecurityWorkflowExecutor::exportWallet( decryptRpc, files, static_cast(std::time(nullptr))); if (!exportOutcome.ok) { std::string err = exportOutcome.error; return [this, err]() { wallet_security_workflow_.fail(err); }; } auto filePlan = exportOutcome.filePlan; return [this, filePlan]() { wallet_security_workflow_.advanceTo(DecryptStep::StopDaemon, services::WalletSecurityWorkflow::stepStatus(DecryptStep::StopDaemon), std::chrono::steady_clock::now()); // Continue with step 3 worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb { WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(), [this](rpc::RPCClient& client, const char* context) { return sendStopCommandSafely(client, context); }); services::WalletSecurityWorkflowExecutor::stopDaemon(decryptRpc); // Wait for daemon to fully stop for (int i = 0; i < 30 && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); return [this, filePlan]() { wallet_security_workflow_.advanceTo(DecryptStep::BackupWallet, services::WalletSecurityWorkflow::stepStatus(DecryptStep::BackupWallet), std::chrono::steady_clock::now()); // Continue with step 4 (rename) worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb { WalletSecurityFileAdapter files; auto backup = services::WalletSecurityWorkflowExecutor::backupEncryptedWallet(files, filePlan); if (!backup.ok) { std::string err = backup.error; return [this, err]() { wallet_security_workflow_.fail(err); }; } return [this, exportPath = filePlan.exportPath]() { wallet_security_workflow_.advanceTo(DecryptStep::RestartDaemon, services::WalletSecurityWorkflow::stepStatus(DecryptStep::RestartDaemon), std::chrono::steady_clock::now()); auto restartAndImport = [this, exportPath](const util::AsyncTaskManager::Token& token) { WalletSecurityDaemonAdapter daemonAdapter(*this, token); WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(), [this](rpc::RPCClient& client, const char* context) { return sendStopCommandSafely(client, context); }); auto restart = services::WalletSecurityWorkflowExecutor::restartDaemonAndWait( daemonAdapter, decryptRpc); if (!restart.ok) { if (restart.error.empty()) return; if (worker_) { worker_->post([this, err = restart.error]() -> rpc::RPCWorker::MainCb { return [this, err]() { wallet_security_workflow_.fail(err); }; }); } return; } // Update step on main thread — close dialog, import in background if (worker_) { worker_->post([this]() -> rpc::RPCWorker::MainCb { return [this]() { // Close the decrypt dialog — user can use the wallet now wallet_security_workflow_.closeDialogForImport(); show_decrypt_dialog_ = false; // Mark rescanning so status bar picks it up immediately state_.sync.rescanning = true; state_.sync.rescan_progress = 0.0f; // Clear encryption state early — vault/PIN removed now, // wallet file is already unencrypted services::WalletSecurityWorkflowExecutor::cleanupVaultAndPin([this]() { if (vault_ && vault_->hasVault()) { vault_->removeVault(); } if (settings_ && settings_->getPinEnabled()) { settings_->setPinEnabled(false); settings_->save(); } }); ui::Notifications::instance().info( "Importing keys & rescanning blockchain — wallet is usable while this runs", 8.0f); }; }); } WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_); auto importResult = services::WalletSecurityWorkflowExecutor::importWallet( importAdapter, exportPath); if (!importResult.ok) { std::string err = importResult.error; if (worker_) { worker_->post([this, err]() -> rpc::RPCWorker::MainCb { return [this, err]() { wallet_security_workflow_.finishImport(); ui::Notifications::instance().error( err + "\nEncrypted backup: wallet.dat.encrypted.bak", 12.0f); }; }); } return; } // Success — force full state refresh so peers, // balances, and addresses are fetched immediately. if (worker_) { worker_->post([this]() -> rpc::RPCWorker::MainCb { return [this]() { wallet_security_workflow_.finishImport(); // Force address + peer refresh invalidateAddressValidationCache(); addresses_dirty_ = true; transactions_dirty_ = true; last_tx_block_height_ = -1; refreshWalletEncryptionState(); refreshData(); refreshPeerInfo(); ui::Notifications::instance().success( "Wallet decrypted successfully! All keys imported.", 8.0f); DEBUG_LOGF("[App] Wallet decrypted successfully\n"); }; }); } }; async_tasks_.submit("decrypt-restart-import", restartAndImport); }; }); }; }); }; }); }; }); } } ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(btnW, 40))) { memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_)); show_decrypt_dialog_ = false; } // ---- Phase 1: Working ---- } else if (decryptState.phase == DecryptPhase::Working) { // Step checklist const char* stepLabels[] = { "Unlocking wallet", "Exporting wallet keys", "Stopping daemon", "Backing up encrypted wallet", "Restarting daemon" }; const int numSteps = 5; // Compute elapsed times auto now = std::chrono::steady_clock::now(); auto stepElapsed = std::chrono::duration_cast( now - decryptState.stepStarted).count(); auto totalElapsed = std::chrono::duration_cast( now - decryptState.overallStarted).count(); ImGui::Spacing(); for (int i = 0; i < numSteps; i++) { ImGui::PushFont(Type().iconMed()); if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step, services::WalletSecurityWorkflow::stepFromIndex(i))) { // Completed ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE); } else if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) { // In progress - animate float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, alpha), ICON_MD_PENDING); } else { // Not started ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.5f), ICON_MD_RADIO_BUTTON_UNCHECKED); } ImGui::PopFont(); ImGui::SameLine(); if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) { // Show step label with elapsed time int mins = (int)(stepElapsed / 60); int secs = (int)(stepElapsed % 60); if (mins > 0) { ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f), "%s... (%dm %02ds)", stepLabels[i], mins, secs); } else { ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f), "%s... (%ds)", stepLabels[i], secs); } } else if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step, services::WalletSecurityWorkflow::stepFromIndex(i))) { ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]); } else { ImGui::TextDisabled("%s", stepLabels[i]); } } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Indeterminate progress bar { float barW = ImGui::GetContentRegionAvail().x; float barH = 6.0f; ImVec2 p = ImGui::GetCursorScreenPos(); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH), IM_COL32(255, 255, 255, 25), 3.0f); float t = (float)ImGui::GetTime(); float segW = barW * 0.3f; float x0 = p.x + (barW + segW) * (0.5f + 0.5f * sinf(t * 2.0f)) - segW; float x1 = x0 + segW; x0 = std::max(x0, p.x); x1 = std::min(x1, p.x + barW); if (x1 > x0) { dl->AddRectFilled(ImVec2(x0, p.y), ImVec2(x1, p.y + barH), IM_COL32(255, 218, 0, 200), 3.0f); } ImGui::Dummy(ImVec2(barW, barH)); } ImGui::Spacing(); // Step-specific hints if (decryptState.step == DecryptStep::RestartDaemon) { ImGui::TextWrapped("Waiting for the daemon to finish starting up..."); } else { ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, " "and re-importing. This may take several minutes."); } // Total elapsed { int tMins = (int)(totalElapsed / 60); int tSecs = (int)(totalElapsed % 60); ImGui::Spacing(); ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs); } // ---- Phase 2: Success ---- } else if (decryptState.phase == DecryptPhase::Success) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "Wallet decrypted successfully!"); ImGui::Spacing(); ImGui::TextWrapped( "Your wallet is now unencrypted. A backup of the encrypted wallet " "was saved as wallet.dat.encrypted.bak in your data directory."); ImGui::Spacing(); if (ImGui::Button("Close", ImVec2(-1, 40))) { show_decrypt_dialog_ = false; } // ---- Phase 3: Error ---- } else if (decryptState.phase == DecryptPhase::Error) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed"); ImGui::Spacing(); ImGui::TextWrapped("%s", decryptState.status.c_str()); ImGui::Spacing(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button("Try Again", ImVec2(btnW, 40))) { wallet_security_workflow_.reset(); } ImGui::SameLine(); if (ImGui::Button("Close", ImVec2(btnW, 40))) { show_decrypt_dialog_ = false; } } EndOverlayDialog(); } } // =========================================================================== // PIN Setup / Change / Remove Dialogs (from Settings page) // =========================================================================== void App::renderPinDialogs() { using namespace ui::material; // ---- Set PIN dialog ---- if (show_pin_setup_) { if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) { ImGui::TextWrapped( "Set a 4-8 digit PIN for quick wallet unlock. " "Your wallet passphrase will be encrypted with this PIN " "and stored locally."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("Wallet Passphrase:"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_), ImGuiInputTextFlags_Password); ImGui::PopItemWidth(); ImGui::Text("New PIN (4-8 digits):"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); ImGui::Text("Confirm PIN:"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); if (!pin_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str()); } ImGui::Spacing(); std::string pinStr(pin_buf_); bool valid = strlen(pin_passphrase_buf_) > 0 && util::SecureVault::isValidPin(pinStr) && strcmp(pin_buf_, pin_confirm_buf_) == 0; ImGui::BeginDisabled(!valid || pin_in_progress_); if (ImGui::Button("Set PIN", ImVec2(-1, 40))) { pin_in_progress_ = true; pin_status_ = "Verifying passphrase..."; // Verify passphrase + store vault on worker thread to avoid // blocking the UI with Argon2id key derivation. std::string passphrase(pin_passphrase_buf_); std::string pin(pin_buf_); memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_)); memset(pin_buf_, 0, sizeof(pin_buf_)); memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_)); if (rpc_ && rpc_->isConnected() && worker_) { worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb { // Verify passphrase via RPC (worker thread) try { rpc::RPCClient::TraceScope trace("Security / PIN setup"); rpc_->call("walletpassphrase", {passphrase, 5}); } catch (const std::exception& e) { return [this]() { pin_status_ = "Incorrect passphrase"; pin_in_progress_ = false; }; } // Passphrase correct — store in vault (Argon2id, worker thread) bool storeOk = vault_ && vault_->store(pin, passphrase); // Lock wallet back try { rpc::RPCClient::TraceScope trace("Security / PIN setup"); rpc_->call("walletlock"); } catch (...) {} return [this, storeOk]() { if (storeOk) { settings_->setPinEnabled(true); settings_->save(); pin_status_.clear(); pin_in_progress_ = false; show_pin_setup_ = false; ui::Notifications::instance().info("PIN set successfully"); } else { pin_status_ = "Failed to create vault"; pin_in_progress_ = false; } }; }); } else { pin_status_ = "Not connected to daemon"; pin_in_progress_ = false; } } ImGui::EndDisabled(); EndOverlayDialog(); } } // ---- Change PIN dialog ---- if (show_pin_change_) { if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) { ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("Current PIN:"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); ImGui::Text("New PIN (4-8 digits):"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); ImGui::Text("Confirm New PIN:"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); if (!pin_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str()); } ImGui::Spacing(); std::string newPin(pin_buf_); bool valid = strlen(pin_old_buf_) >= 4 && util::SecureVault::isValidPin(newPin) && strcmp(pin_buf_, pin_confirm_buf_) == 0; ImGui::BeginDisabled(!valid || pin_in_progress_); if (ImGui::Button("Change PIN", ImVec2(-1, 40))) { pin_in_progress_ = true; pin_status_ = "Changing PIN..."; std::string oldPin(pin_old_buf_); std::string newPinCopy = newPin; memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); memset(pin_buf_, 0, sizeof(pin_buf_)); memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_)); if (worker_ && vault_) { worker_->post([this, oldPin, newPinCopy]() -> rpc::RPCWorker::MainCb { // Argon2id runs here (worker thread) bool ok = vault_->changePin(oldPin, newPinCopy); return [this, ok]() { if (ok) { pin_status_.clear(); pin_in_progress_ = false; show_pin_change_ = false; ui::Notifications::instance().info("PIN changed successfully"); } else { pin_status_ = "Incorrect current PIN"; pin_in_progress_ = false; } }; }); } else { pin_status_ = "Internal error"; pin_in_progress_ = false; } } ImGui::EndDisabled(); EndOverlayDialog(); } } // ---- Remove PIN dialog ---- if (show_pin_remove_) { if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) { ImGui::TextWrapped( "Enter your current PIN to confirm removal. " "You will need to use your full passphrase to unlock."); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("Current PIN:"); ImGui::PushItemWidth(-1); ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal); ImGui::PopItemWidth(); if (!pin_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str()); } ImGui::Spacing(); bool valid = strlen(pin_old_buf_) >= 4; ImGui::BeginDisabled(!valid || pin_in_progress_); if (ImGui::Button("Remove PIN", ImVec2(-1, 40))) { pin_in_progress_ = true; pin_status_ = "Verifying PIN..."; std::string oldPin(pin_old_buf_); memset(pin_old_buf_, 0, sizeof(pin_old_buf_)); if (worker_ && vault_) { worker_->post([this, oldPin]() -> rpc::RPCWorker::MainCb { // Argon2id runs here (worker thread) std::string passphrase; bool ok = vault_->retrieve(oldPin, passphrase); if (ok) { util::SecureVault::secureZero(&passphrase[0], passphrase.size()); } return [this, ok]() { if (ok) { vault_->removeVault(); settings_->setPinEnabled(false); settings_->save(); pin_status_.clear(); pin_in_progress_ = false; show_pin_remove_ = false; ui::Notifications::instance().info("PIN removed"); } else { pin_status_ = "Incorrect PIN"; pin_in_progress_ = false; } }; }); } else { pin_status_ = "Internal error"; pin_in_progress_ = false; } } ImGui::EndDisabled(); EndOverlayDialog(); } } } } // namespace dragonx