feat(settings): gate debug options behind a confirmation (+ re-auth if secured)

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 18:46:50 -05:00
parent aa587d1d84
commit 46fb0029b8
6 changed files with 160 additions and 1 deletions

View File

@@ -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<void(bool)> 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());

View File

@@ -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<void(bool)> 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(); }

View File

@@ -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,

View File

@@ -144,6 +144,13 @@ struct SettingsPageState {
std::set<std::string> 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

View File

@@ -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

View File

@@ -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";