From 46fb0029b828f0699c7e42bf0fce34c152911dc6 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 10 Jul 2026 18:46:50 -0500 Subject: [PATCH] feat(settings): gate debug options behind a confirmation (+ re-auth if secured) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revealing the DEBUG OPTIONS dropdown now opens a confirmation modal with a warning instead of expanding directly. If the wallet is secured, it also requires re-authentication before the options appear: - a quick-unlock PIN → verified against the vault (Argon2id derive, off-thread, no wallet side effects); - otherwise an encrypted wallet → the passphrase, verified via walletpassphrase. Unsecured wallets just confirm. Collapsing needs no gate; each expand re-gates. App gains debugGateRequiresAuth() + verifyDebugCredential() (cb on the main thread; secrets wiped). Adds a modal-debug-gate sweep surface. Verified 100% + 150%, dark + light. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 41 +++++++++++++++ src/app.h | 6 +++ src/app_sweep.cpp | 6 +++ src/ui/pages/settings_page.cpp | 96 +++++++++++++++++++++++++++++++++- src/ui/pages/settings_page.h | 4 ++ src/util/i18n.cpp | 8 +++ 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index 2ecb6a8..2394384 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4731,6 +4731,47 @@ bool App::hasPinVault() const { return vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled(); } +bool App::debugGateRequiresAuth() const { + return hasPinVault() || state_.encrypted; +} + +void App::verifyDebugCredential(const std::string& secret, std::function cb) { + if (!cb) return; + // A quick-unlock PIN vault → verify the PIN by deriving it (pure crypto, no wallet side effects). + if (hasPinVault()) { + auto* w = worker_.get(); + if (!w) { cb(false); return; } + w->post([this, s = secret, cb]() mutable -> rpc::RPCWorker::MainCb { + std::string pass; + bool ok = vault_ && vault_->retrieve(s, pass); + if (!pass.empty()) util::SecureVault::secureZero(pass.data(), pass.size()); + if (!s.empty()) util::SecureVault::secureZero(&s[0], s.size()); + return [cb, ok]() { cb(ok); }; + }); + return; + } + // Encrypted wallet, no PIN → verify the passphrase via walletpassphrase (re-unlocks with the + // usual timeout; a wrong passphrase throws). + if (state_.encrypted) { + auto* w = worker_.get(); + auto* r = rpc_.get(); + if (!w || !r) { cb(false); return; } + int autoLock = settings_ ? settings_->getAutoLockTimeout() : 300; + int timeout = (autoLock > 0) ? std::max(600, autoLock * 2) : 86400; + w->post([r, s = secret, timeout, cb]() mutable -> rpc::RPCWorker::MainCb { + bool ok = false; + try { + if (r && r->isConnected()) { r->call("walletpassphrase", {s, timeout}); ok = true; } + } catch (...) { ok = false; } + if (!s.empty()) util::SecureVault::secureZero(&s[0], s.size()); + return [cb, ok]() { cb(ok); }; + }); + return; + } + // No credential set — nothing to verify. + cb(true); +} + bool App::hasPendingRPCResults() const { return (worker_ && worker_->hasPendingResults()) || (fast_worker_ && fast_worker_->hasPendingResults()); diff --git a/src/app.h b/src/app.h index aa41c54..b82e3d6 100644 --- a/src/app.h +++ b/src/app.h @@ -495,6 +495,12 @@ public: void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); } bool hasPinVault() const; + // Debug-options gate: does revealing the debug dropdown need re-authentication (a PIN vault or + // an encrypted wallet)? And verify the entered PIN/passphrase — cb(ok) is invoked on the main + // thread (PIN via the vault, else the wallet passphrase via RPC). + bool debugGateRequiresAuth() const; + void verifyDebugCredential(const std::string& secret, std::function cb); + /// @brief Check if RPC worker has queued results waiting to be processed bool hasPendingRPCResults() const; bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); } diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index 17b9f7f..f1bfc6b 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -21,6 +21,7 @@ #include "ui/windows/wallets_dialog.h" #include "ui/windows/daemon_download_dialog.h" #include "ui/windows/xmrig_download_dialog.h" +#include "ui/pages/settings_page.h" #include "util/platform.h" #include "wallet/wallet_capabilities.h" @@ -442,6 +443,11 @@ void App::buildSweepCatalog() [](App& a) { a.console_tab_.sweepSetCommandsPopup(true); }, [](App& a) { a.console_tab_.sweepSetCommandsPopup(false); }); + // Debug-options gate: confirmation + warning, with the passphrase re-auth field (encrypted). + add("modal-debug-gate", ui::NavPage::Settings, + [](App& a) { a.state_.encrypted = true; ui::SweepOpenDebugGate(true); }, + [](App& a) { ui::SweepOpenDebugGate(false); a.applyHealthyDemoState(); }); + // Daemon updater — the two-pane version picker (versions left, selected-version detail right). // Seeds fake releases so the offline sweep renders it without a network fetch / live updater. add("modal-daemon-update", ui::NavPage::Settings, diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index ccc4c99..e84988e 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -144,6 +144,13 @@ struct SettingsPageState { std::set debug_categories; bool debug_cats_dirty = false; bool debug_expanded = false; + // Debug-options gate: a confirmation + warning (and, when a PIN/passphrase is set, re-auth) is + // required before the debug dropdown reveals its options. + bool debug_gate_open = false; + bool debug_gate_verifying = false; + char debug_gate_buf[128] = {0}; + std::string debug_gate_err; + float debug_gate_err_timer = 0.0f; bool effects_expanded = false; bool tools_expanded = false; bool rpc_expanded = false; // Node & Security: reveal the RPC connection fields @@ -2521,7 +2528,15 @@ void RenderSettingsPage(App* app) { ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) { - s_settingsState.debug_expanded = !s_settingsState.debug_expanded; + if (s_settingsState.debug_expanded) { + s_settingsState.debug_expanded = false; // collapsing needs no gate + } else { + // Expanding is gated: open the confirmation + (if a PIN/passphrase is set) re-auth. + s_settingsState.debug_gate_open = true; + s_settingsState.debug_gate_buf[0] = '\0'; + s_settingsState.debug_gate_err.clear(); + s_settingsState.debug_gate_verifying = false; + } } if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand")); ImGui::PopStyleColor(3); @@ -2647,6 +2662,76 @@ void RenderSettingsPage(App* app) { if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon")); } } + + // ---- Debug-options gate: confirmation + warning (+ re-auth if a PIN/passphrase is set) ---- + if (s_settingsState.debug_gate_open) { + auto& st = s_settingsState; + if (st.debug_gate_err_timer > 0.0f) { + st.debug_gate_err_timer -= ImGui::GetIO().DeltaTime; + if (st.debug_gate_err_timer <= 0.0f) st.debug_gate_err.clear(); + } + const bool needAuth = app->debugGateRequiresAuth(); + const bool hasPin = app->hasPinVault(); + const float gdp = Layout::dpiScale(); + material::OverlayDialogSpec ov; + ov.title = TR("debug_gate_title"); + ov.p_open = &st.debug_gate_open; + ov.style = material::OverlayStyle::BlurFloat; + ov.cardWidth = 480.0f; + ov.idSuffix = "debuggate"; + if (material::BeginOverlayDialog(ov)) { + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) st.debug_gate_open = false; + ImGui::TextWrapped("%s", TR("debug_gate_warning")); + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + + bool submit = false; + if (needAuth) { + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), + hasPin ? TR("debug_gate_pin_prompt") : TR("debug_gate_pass_prompt")); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGuiInputTextFlags f = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue; + if (hasPin) f |= ImGuiInputTextFlags_CharsDecimal; // PIN is numeric + if (ImGui::InputText("##debugGateSecret", st.debug_gate_buf, sizeof(st.debug_gate_buf), f)) + submit = true; + if (!st.debug_gate_err.empty()) { + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + Type().textColored(TypeStyle::Caption, Error(), st.debug_gate_err.c_str()); + } + } + + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::BeginDisabled(st.debug_gate_verifying); + if (TactileButton(TR("debug_gate_confirm"), ImVec2(170.0f * gdp, 0)) || submit) { + if (!needAuth) { + st.debug_expanded = true; + st.debug_gate_open = false; + } else if (strlen(st.debug_gate_buf) > 0) { + st.debug_gate_verifying = true; + std::string secret = st.debug_gate_buf; + memset(st.debug_gate_buf, 0, sizeof(st.debug_gate_buf)); + app->verifyDebugCredential(secret, [](bool ok) { + auto& s = s_settingsState; + s.debug_gate_verifying = false; + if (ok) { s.debug_expanded = true; s.debug_gate_open = false; } + else { s.debug_gate_err = TR("debug_gate_incorrect"); s.debug_gate_err_timer = 4.0f; } + }); + if (!secret.empty()) memset(&secret[0], 0, secret.size()); + } + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (TactileButton(TR("cancel"), ImVec2(120.0f * gdp, 0))) { + memset(st.debug_gate_buf, 0, sizeof(st.debug_gate_buf)); + st.debug_gate_open = false; + } + if (st.debug_gate_verifying) { + ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("debug_gate_verifying")); + } + material::EndOverlayDialog(); + } + } } // --- Shader-based scroll fade: unbind (restore ImGui's default shader) --- @@ -2923,5 +3008,14 @@ void RenderSettingsPage(App* app) { } +void SweepOpenDebugGate(bool open) { + s_settingsState.debug_gate_open = open; + if (open) { + s_settingsState.debug_gate_buf[0] = '\0'; + s_settingsState.debug_gate_err.clear(); + s_settingsState.debug_gate_verifying = false; + } +} + } // namespace ui } // namespace dragonx diff --git a/src/ui/pages/settings_page.h b/src/ui/pages/settings_page.h index dd1c41d..912861e 100644 --- a/src/ui/pages/settings_page.h +++ b/src/ui/pages/settings_page.h @@ -15,5 +15,9 @@ namespace ui { */ void RenderSettingsPage(App* app); +// Sweep-only: force the debug-options confirmation gate open/closed so the offline UI sweep can +// capture it (it is otherwise opened only by clicking the DEBUG OPTIONS header). +void SweepOpenDebugGate(bool open); + } // namespace ui } // namespace dragonx diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 3ab923f..c58a26b 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -666,6 +666,14 @@ void I18n::loadBuiltinEnglish() strings_["debug_logging"] = "DEBUG OPTIONS"; strings_["settings_debug_select"] = "Select categories to enable daemon debug logging (-debug= flags)."; strings_["settings_debug_restart_note"] = "Changes take effect after restarting the daemon."; + // Debug-options gate (confirmation + optional re-auth) + strings_["debug_gate_title"] = "Show debug options?"; + strings_["debug_gate_warning"] = "These are advanced diagnostic and node-tuning options. They can expose sensitive information and change how your node runs. Only continue if you know what you're doing."; + strings_["debug_gate_pin_prompt"] = "Enter your unlock PIN to continue:"; + strings_["debug_gate_pass_prompt"] = "Enter your wallet passphrase to continue:"; + strings_["debug_gate_confirm"] = "Show debug options"; + strings_["debug_gate_incorrect"] = "Incorrect PIN or passphrase."; + strings_["debug_gate_verifying"] = "Verifying\xE2\x80\xA6"; // Settings window (legacy dialog) descriptions strings_["settings_language_note"] = "Note: Some text requires restart to update";