// 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 "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 "util/platform.h" #include "util/secure_vault.h" #include "util/perf_log.h" #include "embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include namespace dragonx { using json = nlohmann::json; // =========================================================================== // 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]() -> rpc::RPCWorker::MainCb { try { auto result = rpc_->call("encryptwallet", {passphrase}); 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"); // The daemon shuts itself down after encryptwallet if (isUsingEmbeddedDaemon()) { // Give daemon a moment to shut down, then restart // (do this off the main thread to avoid stalling the UI) std::thread([this]() { std::this_thread::sleep_for(std::chrono::seconds(2)); stopEmbeddedDaemon(); startEmbeddedDaemon(); // tryConnect will be called by the update loop }).detach(); } else { ui::Notifications::instance().warning( "Please restart your daemon for encryption to take effect."); } }; } catch (const std::exception& e) { std::string err = e.what(); 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 (!deferred_encrypt_pending_) return; // Phase 1: wait for daemon connection if (!state_.connected || !rpc_ || !rpc_->isConnected()) { // Throttle connection attempts to every 3 seconds static double s_lastAttempt = -10.0; double now = ImGui::GetTime(); if (now - s_lastAttempt >= 3.0) { s_lastAttempt = now; 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_) { std::string passphrase = deferred_encrypt_passphrase_; std::string pin = deferred_encrypt_pin_; encrypt_in_progress_ = true; encrypt_status_ = "Encrypting wallet..."; if (worker_) { worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb { try { rpc_->call("encryptwallet", {passphrase}); // Store PIN vault on the worker thread (Argon2id is expensive) bool pinStored = false; if (!pin.empty() && vault_) { pinStored = vault_->store(pin, passphrase); } return [this, pinStored, pin]() { encrypt_in_progress_ = false; encrypt_status_.clear(); DEBUG_LOGF("[App] Wallet encrypted (deferred)\n"); // Finalize PIN settings on main thread if (!pin.empty()) { if (pinStored) { settings_->setPinEnabled(true); settings_->save(); ui::Notifications::instance().info("Wallet encrypted & PIN set"); } else { ui::Notifications::instance().warning( "Wallet encrypted but PIN vault failed"); } } else { ui::Notifications::instance().info("Wallet encrypted successfully"); } // Securely clear deferred state if (!deferred_encrypt_passphrase_.empty()) { util::SecureVault::secureZero( &deferred_encrypt_passphrase_[0], deferred_encrypt_passphrase_.size()); deferred_encrypt_passphrase_.clear(); } if (!deferred_encrypt_pin_.empty()) { util::SecureVault::secureZero( &deferred_encrypt_pin_[0], deferred_encrypt_pin_.size()); deferred_encrypt_pin_.clear(); } deferred_encrypt_pending_ = false; // Restart daemon (it shuts itself down after encryptwallet) if (isUsingEmbeddedDaemon()) { std::thread([this]() { std::this_thread::sleep_for(std::chrono::seconds(2)); stopEmbeddedDaemon(); startEmbeddedDaemon(); // tryConnect will be called by the update loop }).detach(); } else { ui::Notifications::instance().warning( "Please restart your daemon for encryption to take effect."); } }; } catch (const std::exception& e) { std::string err = e.what(); return [this, err]() { encrypt_in_progress_ = false; encrypt_status_ = "Encryption failed: " + err; deferred_encrypt_pending_ = false; DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str()); ui::Notifications::instance().error("Encryption failed: " + err); // Clean up sensitive data on failure if (!deferred_encrypt_passphrase_.empty()) { util::SecureVault::secureZero( &deferred_encrypt_passphrase_[0], deferred_encrypt_passphrase_.size()); deferred_encrypt_passphrase_.clear(); } if (!deferred_encrypt_pin_.empty()) { util::SecureVault::secureZero( &deferred_encrypt_pin_[0], deferred_encrypt_pin_.size()); deferred_encrypt_pin_.clear(); } }; } }); } } } 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, timeout]() -> rpc::RPCWorker::MainCb { bool ok = false; std::string err_msg; try { r->call("walletpassphrase", {passphrase, timeout}); ok = true; } catch (const std::exception& e) { err_msg = e.what(); } return [this, ok, err_msg, timeout]() { 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; } else { lock_attempts_++; lock_error_msg_ = "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()); } }; }); } 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; 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, newPass]() -> 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(); } return [this, ok, err_msg]() { 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_)); ui::Notifications::instance().info("Passphrase changed successfully"); } else { encrypt_status_ = "Failed: " + err_msg; } }; }); } // =========================================================================== // 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 { 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); } else { state_.encrypted = false; state_.locked = false; state_.unlocked_until = 0; // 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(); } } state_.encryption_state_known = true; } 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); } } // =========================================================================== // 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) { return [this]() { lock_unlock_in_progress_ = false; lock_attempts_++; lock_error_msg_ = "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(); } // Securely wipe passphrase util::SecureVault::secureZero(&passphrase[0], passphrase.size()); if (rpcOk) { return [this, timeout]() { 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; }; } else { return [this, rpcErr]() { lock_unlock_in_progress_ = false; lock_attempts_++; lock_error_msg_ = "Unlock failed: " + rpcErr; lock_error_timer_ = 3.0f; }; } }); } } 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; bool canClose = (decrypt_phase_ != 1); // don't close while working bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr; if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) { // ---- Phase 0: Passphrase entry ---- if (decrypt_phase_ == 0) { 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 (!decrypt_status_.empty()) { ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decrypt_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 || decrypt_in_progress_); 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_)); decrypt_phase_ = 1; decrypt_step_ = 0; decrypt_in_progress_ = true; decrypt_status_ = "Unlocking wallet..."; decrypt_overall_start_time_ = std::chrono::steady_clock::now(); decrypt_step_start_time_ = decrypt_overall_start_time_; // Run entire decrypt flow on worker thread if (worker_) { worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb { // Step 1: Unlock wallet try { rpc_->call("walletpassphrase", {passphrase, 600}); } catch (const std::exception& e) { std::string err = e.what(); return [this, err]() { decrypt_in_progress_ = false; decrypt_status_ = "Incorrect passphrase"; decrypt_phase_ = 0; // back to entry }; } // Update step on main thread return [this]() { decrypt_step_ = 1; decrypt_step_start_time_ = std::chrono::steady_clock::now(); decrypt_status_ = "Exporting wallet keys..."; // Continue with step 2 worker_->post([this]() -> rpc::RPCWorker::MainCb { std::string dataDir = util::Platform::getDragonXDataDir(); std::string exportFile = "obsidiandecryptexport" + std::to_string(std::time(nullptr)); std::string exportPath = dataDir + exportFile; try { rpc_->call("z_exportwallet", {exportFile}, 300L); } catch (const std::exception& e) { std::string err = e.what(); return [this, err]() { decrypt_in_progress_ = false; decrypt_status_ = "Export failed: " + err; decrypt_phase_ = 3; }; } return [this, exportPath]() { decrypt_step_ = 2; decrypt_step_start_time_ = std::chrono::steady_clock::now(); decrypt_status_ = "Stopping daemon..."; // Continue with step 3 worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb { try { rpc_->call("stop"); } catch (...) { // stop often throws because connection drops } // Wait for daemon to fully stop std::this_thread::sleep_for(std::chrono::seconds(3)); return [this, exportPath]() { decrypt_step_ = 3; decrypt_step_start_time_ = std::chrono::steady_clock::now(); decrypt_status_ = "Backing up encrypted wallet..."; // Continue with step 4 (rename) worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb { std::string dataDir = util::Platform::getDragonXDataDir(); std::string walletPath = dataDir + "wallet.dat"; std::string backupPath = dataDir + "wallet.dat.encrypted.bak"; std::error_code ec; if (std::filesystem::exists(walletPath, ec)) { std::filesystem::remove(backupPath, ec); std::filesystem::rename(walletPath, backupPath, ec); if (ec) { std::string err = ec.message(); return [this, err]() { decrypt_in_progress_ = false; decrypt_status_ = "Failed to rename wallet.dat: " + err; decrypt_phase_ = 3; }; } } return [this, exportPath]() { decrypt_step_ = 4; decrypt_step_start_time_ = std::chrono::steady_clock::now(); decrypt_status_ = "Restarting daemon..."; auto restartAndImport = [this, exportPath]() { std::this_thread::sleep_for(std::chrono::seconds(2)); if (isUsingEmbeddedDaemon()) { stopEmbeddedDaemon(); std::this_thread::sleep_for(std::chrono::seconds(1)); startEmbeddedDaemon(); } // Wait for daemon to become available int maxWait = 60; bool daemonUp = false; for (int i = 0; i < maxWait; i++) { std::this_thread::sleep_for(std::chrono::seconds(1)); try { rpc_->call("getinfo"); daemonUp = true; break; } catch (...) {} } if (!daemonUp) { if (worker_) { worker_->post([this]() -> rpc::RPCWorker::MainCb { return [this]() { decrypt_in_progress_ = false; decrypt_status_ = "Daemon failed to restart"; decrypt_phase_ = 3; }; }); } 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 decrypt_in_progress_ = false; show_decrypt_dialog_ = false; decrypt_import_active_ = true; // 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 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); }; }); } // Step 6: Import wallet in background (use full path) // Use a SEPARATE RPC client so the main rpc_'s // curl_mutex isn't held for the entire import duration. // Blocking rpc_ prevents refreshData/refreshPeerInfo // from running, which leaves the UI with no peers. auto importRpc = std::make_unique(); bool importRpcOk = importRpc->connect( saved_config_.host, saved_config_.port, saved_config_.rpcuser, saved_config_.rpcpassword); if (!importRpcOk) { // Fall back to main client if temp connect fails importRpc.reset(); } auto* rpcForImport = importRpc ? importRpc.get() : rpc_.get(); // Use 20-minute timeout — import + rescan can be very slow try { rpcForImport->call("z_importwallet", {exportPath}, 1200L); } catch (const std::exception& e) { std::string err = e.what(); if (worker_) { worker_->post([this, err]() -> rpc::RPCWorker::MainCb { return [this, err]() { decrypt_import_active_ = false; ui::Notifications::instance().error( "Key import failed: " + err + "\nEncrypted backup: wallet.dat.encrypted.bak", 12.0f); }; }); } return; } // Disconnect the temporary RPC client if (importRpc) { importRpc->disconnect(); importRpc.reset(); } // Success — force full state refresh so peers, // balances, and addresses are fetched immediately. if (worker_) { worker_->post([this]() -> rpc::RPCWorker::MainCb { return [this]() { decrypt_import_active_ = false; // Force address + peer refresh 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"); }; }); } }; std::thread(restartAndImport).detach(); }; }); }; }); }; }); }; }); } } 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 (decrypt_phase_ == 1) { // 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 - decrypt_step_start_time_).count(); auto totalElapsed = std::chrono::duration_cast( now - decrypt_overall_start_time_).count(); ImGui::Spacing(); for (int i = 0; i < numSteps; i++) { ImGui::PushFont(Type().iconMed()); if (i < decrypt_step_) { // Completed ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE); } else if (i == decrypt_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 == decrypt_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 (i < decrypt_step_) { 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 (decrypt_step_ == 4) { 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 (decrypt_phase_ == 2) { 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 (decrypt_phase_ == 3) { 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", decrypt_status_.c_str()); ImGui::Spacing(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button("Try Again", ImVec2(btnW, 40))) { decrypt_phase_ = 0; decrypt_step_ = 0; decrypt_status_.clear(); } 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_->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_->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