feat(security): persist unlock lockout across restarts; keep unlock errors visible

- The failed-unlock error message now stays on screen until the next attempt /
  mode switch / success, instead of fading after ~3s (the reason was often gone
  before it could be read).
- The escalating failed-attempt lockout counter + expiry are now persisted
  (keyed to the active wallet file) and restored once per lock-screen session,
  so killing and relaunching the app can no longer reset the lockout curve — a
  brute-force window. A forward clock change is capped; the PIN/passphrase is
  never stored. The verification (walletpassphrase/decrypt) path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 23:20:32 -05:00
co-authored by Claude Opus 4.8
parent 89d3975eb3
commit a30a083df0
4 changed files with 66 additions and 4 deletions
+12
View File
@@ -393,6 +393,15 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// Unlock lockout state, persisted across restarts so the failed-attempt curve can't be reset by
// killing the app. Keyed by the wallet file it applies to; the PIN/passphrase is never stored.
int getLockFailedAttempts() const { return lock_failed_attempts_; }
void setLockFailedAttempts(int v) { lock_failed_attempts_ = v; durable_next_save_ = true; }
long long getLockoutUntilEpoch() const { return lockout_until_epoch_; }
void setLockoutUntilEpoch(long long v) { lockout_until_epoch_ = v; durable_next_save_ = true; }
std::string getLockStateWallet() const { return lock_state_wallet_; }
void setLockStateWallet(const std::string& v) { lock_state_wallet_ = v; durable_next_save_ = true; }
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
bool getLargeWalletWarned() const { return large_wallet_warned_; }
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
@@ -736,6 +745,9 @@ private:
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
int lock_failed_attempts_ = 0; // persisted unlock-failure count (lockout curve)
long long lockout_until_epoch_ = 0; // unix time the current lockout expires (0 = none)
std::string lock_state_wallet_; // wallet file the above two apply to
bool large_wallet_warned_ = false;
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
bool encryption_pending_ = false;