fix(security): don't silently leave a wallet unencrypted or unlocked (W2-2, W2-4)

P0-B encryption-integrity cluster.

W2-2: the first-run wizard's "encrypt" stored the passphrase only in memory and let the
user into the app immediately, so a quit/crash or a failed daemon connect before the
deferred encryption applied left the wallet unencrypted with NO record encryption was
ever requested — the user believing it was encrypted. A persisted encryption_pending
settings flag is now set the moment encryption is requested (never the passphrase, only
the fact). refreshWalletEncryptionState() reconciles it on every connect: wallet observed
encrypted -> clear the flag; wallet NOT encrypted while the flag is set and no deferred
encryption is pending/in-flight -> a once-per-session "your wallet is NOT encrypted — open
Settings to finish" warning (the flag stays set, so it recurs each launch until resolved).
The passphrase is deliberately never persisted to auto-complete — surfacing it is the
secure choice.

W2-4: lockWallet()'s continuation only handled success — a failed walletlock RPC silently
left the wallet UNLOCKED (an unfulfilled auto-lock). It now logs and warns once (reset on
the next successful lock) so a failing auto-lock is visible instead of leaving the wallet
exposed.

Touches settings.{h,cpp}, app_wizard.cpp, app_security.cpp, app.h. Not unit-testable at
this layer (RPC/connect-driven state). Build-clean; ctest 1/1. See docs/wallet-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 14:48:02 -05:00
co-authored by Claude Opus 4.8
parent c7d163f44a
commit 8c12b27c0a
6 changed files with 50 additions and 1 deletions
+7
View File
@@ -327,6 +327,12 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// Persisted the moment deferred (wizard) encryption is requested; cleared only once the wallet is
// observed to be actually encrypted. Lets a quit/crash/failed-connect before it applies be detected
// and surfaced (W2-2). NEVER stores the passphrase — only the fact that encryption was requested.
bool getEncryptionPending() const { return encryption_pending_; }
void setEncryptionPending(bool v) { encryption_pending_ = v; }
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
@@ -574,6 +580,7 @@ private:
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
bool encryption_pending_ = false;
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
bool seed_migration_pending_ = false;