fix(wallet-switch): cluster A — identity/secret teardown on wallet switch

From the wallet-switching audit (HIGH-severity cross-wallet leaks):

- Chat identity leak: the full-node switch (switchToWallet) and seed-migration
  adopt reset state_ but NOT the HushChat identity, so wallet A's decrypted
  conversations surfaced under wallet B and outgoing chat was signed with A's
  keypair. Factor the existing teardown into App::resetChatSession() and call it
  on both wallet-change paths (the lite path already reset it via
  rebuildLiteWallet, which now uses the helper too).
- Global PIN vault: the PIN quick-unlock vault was a single vault.dat, so after
  a switch wallet A's stored passphrase was offered/applied to encrypted wallet
  B. SecureVault is now scoped per wallet (vault-<walletfile>.dat); the default
  wallet keeps the legacy vault.dat for back-compat. vault_ is constructed for
  the active wallet and re-scoped on switch, so B has its own (empty) vault.
- Lock-screen state: switching now clears the carried-over failed-attempt
  counter + lockout timer and secure-zeroes the passphrase/PIN entry buffers so
  the previous wallet's unlock state can't apply to the new one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 12:29:45 -05:00
parent 6f0f95f4fb
commit f546d3e2b1
5 changed files with 67 additions and 21 deletions

View File

@@ -9,6 +9,7 @@
#include <fstream>
#include <filesystem>
#include <cctype>
#include <cstring>
#include <algorithm>
#include "../util/logger.h"
@@ -31,18 +32,31 @@ static constexpr uint8_t VAULT_VERSION = 0x01;
static constexpr unsigned long long ARGON2_MEMLIMIT = crypto_pwhash_MEMLIMIT_MODERATE;
static constexpr unsigned long long ARGON2_OPSLIMIT = crypto_pwhash_OPSLIMIT_MODERATE;
SecureVault::SecureVault() {
SecureVault::SecureVault(const std::string& walletFile) {
// Ensure libsodium is initialized
if (sodium_init() < 0) {
// sodium_init returns 0 on success, 1 if already initialized, -1 on failure
// We'll proceed anyway — the functions will fail gracefully
}
setWalletScope(walletFile);
}
SecureVault::~SecureVault() = default;
std::string SecureVault::getVaultPath() {
return (fs::path(Platform::getConfigDir()) / "vault.dat").string();
void SecureVault::setWalletScope(const std::string& walletFile) {
// The default wallet keeps the legacy global "vault.dat" (empty scope) for backward compatibility;
// any other wallet gets a filesystem-safe per-wallet tag so its vault is separate.
if (walletFile.empty() || walletFile == "wallet.dat") { scope_.clear(); return; }
std::string s;
s.reserve(walletFile.size());
for (unsigned char c : walletFile)
s += (std::isalnum(c) || c == '-' || c == '_') ? static_cast<char>(c) : '_';
scope_ = s;
}
std::string SecureVault::getVaultPath() const {
const std::string name = scope_.empty() ? std::string("vault.dat") : ("vault-" + scope_ + ".dat");
return (fs::path(Platform::getConfigDir()) / name).string();
}
bool SecureVault::hasVault() const {