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
+6
View File
@@ -271,6 +271,9 @@ bool Settings::load(const std::string& path)
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
loadScalar(j, "lock_failed_attempts", lock_failed_attempts_);
loadScalar(j, "lockout_until_epoch", lockout_until_epoch_);
loadScalar(j, "lock_state_wallet", lock_state_wallet_);
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
empty_wallet_warning_acked_.clear();
@@ -596,6 +599,9 @@ bool Settings::save(const std::string& path)
}
j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_;
j["lock_failed_attempts"] = lock_failed_attempts_;
j["lockout_until_epoch"] = lockout_until_epoch_;
j["lock_state_wallet"] = lock_state_wallet_;
j["large_wallet_warned"] = large_wallet_warned_;
j["empty_wallet_warning_acked"] = json::array();
for (const auto& w : empty_wallet_warning_acked_)