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:
41
src/app.cpp
41
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<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());
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user