diff --git a/src/app.cpp b/src/app.cpp index fbd35a5..c1b50e4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -334,8 +334,9 @@ bool App::init() // Ensure ObsidianDragon config directory and template files exist util::Platform::ensureObsidianDragonSetup(); - // Initialize PIN vault - vault_ = std::make_unique(); + // Initialize PIN vault, scoped to the active wallet so one wallet's stored passphrase is never + // offered for another (the default wallet keeps the legacy vault.dat). + vault_ = std::make_unique(settings_ ? settings_->getActiveWalletFile() : ""); // Theme is now applied via SkinManager below after UISchema loads. // The old SetThemeById() C++ fallback is no longer needed at startup @@ -634,12 +635,7 @@ void App::rebuildLiteWallet(bool force) // A rebuilt controller may back a different wallet (server switch / re-open); drop any chat // identity + decrypted messages, lock the DB, and re-arm provisioning so it re-derives (and // reloads the right wallet's history) from the newly opened wallet's seed. - chat_service_.clearIdentity(); - chat_service_.store().clear(); - chat_db_.lock(); - chat_identity_provisioned_ = false; - chat_identity_fetch_in_flight_ = false; - chat_identity_unavailable_ = false; + resetChatSession(); } void App::update() diff --git a/src/app.h b/src/app.h index 4bac1c1..281559b 100644 --- a/src/app.h +++ b/src/app.h @@ -658,6 +658,10 @@ private: // variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the // feature is off, already provisioned, in flight, or unavailable. void maybeProvisionChatIdentity(); + // Drop the in-memory chat identity + decrypted message store, lock the chat DB, and re-arm + // provisioning. MUST be called whenever the loaded wallet changes (switch / seed-migration adopt) + // so wallet A's private chat can't surface — or be signed with A's keys — under wallet B. + void resetChatSession(); // One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per // install) to back up their seed phrase. Cheap early-outs keep it idle until it can act. void maybeRemindSeedBackup(); diff --git a/src/app_network.cpp b/src/app_network.cpp index e9e2b70..6d0952b 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1046,6 +1046,17 @@ void App::switchToWallet(const std::string& walletFile) settings_->setActiveWalletFile(walletFile); settings_->save(); + // Re-scope the PIN vault to the new wallet (its own vault-.dat), and clear any carried-over + // lock-screen attempt/lockout state + wipe the passphrase/PIN entry buffers — the previous wallet's + // failed-unlock counter and typed secrets must not apply to the new wallet. + if (vault_) vault_->setWalletScope(walletFile); + lock_attempts_ = 0; + lock_lockout_timer_ = 0.0f; + lock_error_msg_.clear(); + lock_error_timer_ = 0.0f; + util::SecureVault::secureZero(lock_passphrase_buf_, sizeof(lock_passphrase_buf_)); + util::SecureVault::secureZero(lock_pin_buf_, sizeof(lock_pin_buf_)); + // Same restart coordination as the seed-adopt flow: gate the main-loop reconnect, disconnect, // then stop + restart on a background thread. syncSettings() re-reads active_wallet_file on // start, so the daemon comes up on -wallet=. WalletState clears on disconnect and the @@ -1053,6 +1064,10 @@ void App::switchToWallet(const std::string& walletFile) daemon_restarting_ = true; if (rpc_ && rpc_->isConnected()) rpc_->disconnect(); onDisconnected("Switching wallet"); + // The loaded wallet is changing — drop the old wallet's chat identity + decrypted store so it + // can't surface (or sign outgoing chat) under the new wallet. It re-provisions from the new + // wallet's seed once it connects. onDisconnected alone doesn't cover chat. + resetChatSession(); ui::Notifications::instance().info("Switching wallet — the node will restart…"); async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) { @@ -2506,6 +2521,18 @@ void App::provisionChatIdentityFromSecret(std::string secret) // tick; cheap early-outs keep it idle until it can act. Both variants derive the SAME identity // because they feed the SAME SDXLite-compatible mnemonic into the KDF (identity derivation is // local — peers only ever exchange public keys). +void App::resetChatSession() +{ + if (!chat::hushChatFeatureEnabledAtBuild()) return; // constexpr — folds away in OFF builds + // Drop identity + decrypted plaintext in RAM, lock the seed-encrypted DB, re-arm provisioning. + chat_service_.clearIdentity(); + chat_service_.store().clear(); + chat_db_.lock(); + chat_identity_provisioned_ = false; + chat_identity_fetch_in_flight_ = false; + chat_identity_unavailable_ = false; +} + void App::maybeProvisionChatIdentity() { if (!chat::hushChatFeatureEnabledAtBuild()) return; // constexpr — folds away in OFF builds @@ -2514,13 +2541,7 @@ void App::maybeProvisionChatIdentity() // in-memory identity and re-arm so it re-derives on the next unlock. Unencrypted wallets report // isLocked()==false and fall through. if (state_.isLocked()) { - if (chat_identity_provisioned_ || chat_service_.hasIdentity()) { - chat_service_.clearIdentity(); - chat_service_.store().clear(); // decrypted plaintext in RAM — drop it; DB reloads on unlock - chat_db_.lock(); - chat_identity_provisioned_ = false; - chat_identity_unavailable_ = false; - } + if (chat_identity_provisioned_ || chat_service_.hasIdentity()) resetChatSession(); return; } @@ -3429,6 +3450,9 @@ void App::beginAdoptSeedWallet() daemon_restarting_ = true; if (rpc_ && rpc_->isConnected()) rpc_->disconnect(); onDisconnected("Adopting seed wallet"); + // Adopting swaps in a brand-new seed wallet — drop the legacy wallet's chat identity + store so + // it re-derives from the new seed (the file name is unchanged, so nothing else detects the swap). + resetChatSession(); const std::string base = seed_migration_temp_dir_; async_tasks_.submit("Adopt seed wallet", [this, base](const util::AsyncTaskManager::Token&) { namespace fs = std::filesystem; diff --git a/src/util/secure_vault.cpp b/src/util/secure_vault.cpp index e8a885c..04ea30a 100644 --- a/src/util/secure_vault.cpp +++ b/src/util/secure_vault.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #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(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 { diff --git a/src/util/secure_vault.h b/src/util/secure_vault.h index 621af13..739fc1d 100644 --- a/src/util/secure_vault.h +++ b/src/util/secure_vault.h @@ -29,9 +29,15 @@ namespace util { */ class SecureVault { public: - SecureVault(); + // `walletFile` scopes the vault to a specific wallet.dat so one wallet's PIN passphrase is never + // offered for another. The default wallet ("wallet.dat" / empty) keeps the legacy global vault.dat + // path for backward compatibility; other wallets get their own vault-.dat. + explicit SecureVault(const std::string& walletFile = ""); ~SecureVault(); + // Re-scope to a different wallet (called when the active wallet changes). + void setWalletScope(const std::string& walletFile); + /** * @brief Check if a vault file exists on disk */ @@ -78,15 +84,17 @@ public: static void secureZero(void* ptr, size_t len); /** - * @brief Get the vault file path + * @brief Get this vault's file path (scoped to its wallet) */ - static std::string getVaultPath(); + std::string getVaultPath() const; private: // Derive a 32-byte key from PIN + salt using Argon2id bool deriveKey(const std::string& pin, const uint8_t* salt, size_t saltLen, uint8_t* key, size_t keyLen) const; + + std::string scope_; // "" = default/legacy wallet (vault.dat); else a sanitized wallet-file tag }; } // namespace util