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());