From f5bde67f64e41ee0b663f0edb9443ba53cc32a63 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 6 Jul 2026 19:51:44 -0500 Subject: [PATCH] fix(chat): tear down the chat session immediately on lite wallet lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security-review finding (low): on the lite variant, "Lock now" ran the backend `lock` but never updated state_.locked — that only refreshes on the next ~2s poll. Since chat teardown is driven by state_.isLocked() in maybeProvision ChatIdentity, the decrypted in-memory store, the chat identity secret key, and the seed-derived AEAD DB key all lingered in RAM for up to ~2s past an explicit lock (the full-node path closes this immediately, since App::lockWallet sets state_.locked synchronously). New App::lockLiteWallet() mirrors the full-node behavior: on a successful lite lock it sets state_.locked and tears down the chat session now (clearIdentity + store().clear() + chat_db_.lock() + re-arm). The lite "Lock now" button routes through it. (Note: the lite variant has no auto-lock at all — checkAutoLock -> App::lockWallet early-returns for lite — which is a separate, general lite gap.) Completes the end-to-end feature security review (1 confirmed finding of 3 raw, now fixed). Verified: Linux + Windows build with chat ON, ctest 100%, hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.h | 3 +++ src/app_network.cpp | 19 +++++++++++++++++++ src/ui/pages/settings_page.cpp | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/app.h b/src/app.h index 308e3d7..cd116ab 100644 --- a/src/app.h +++ b/src/app.h @@ -182,6 +182,9 @@ public: const std::string& liteOpenError() const { return lite_open_error_; } // Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet). void requestLiteUnlock() { lite_unlock_prompt_ = true; } + // Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock` + // doesn't update state_.locked until the next poll, so chat secrets would otherwise linger). + bool lockLiteWallet(); // (Re)build the lite controller from current settings so a changed lite-server selection // takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp). void rebuildLiteWallet(bool force = false); diff --git a/src/app_network.cpp b/src/app_network.cpp index 2f910e5..e2a9128 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -2660,6 +2660,25 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model) if (!metadata.empty()) chat_service_.ingest(metadata, txTimestamps, std::time(nullptr)); } +bool App::lockLiteWallet() +{ + if (!lite_wallet_) return false; + const bool ok = lite_wallet_->lockWallet(); + if (ok) { + // Full-node App::lockWallet() sets state_.locked synchronously; the lite backend `lock` + // doesn't, and state_.locked only refreshes on the next ~2s poll. Mirror the full-node + // behavior here and tear down the chat session NOW so no decrypted store / unlocked DB key + // lingers past an explicit lock. (These chat calls are safe no-ops when chat is off/idle.) + state_.locked = true; + chat_service_.clearIdentity(); + chat_service_.store().clear(); + chat_db_.lock(); + chat_identity_provisioned_ = false; + chat_identity_unavailable_ = false; + } + return ok; +} + void App::seedChatDemoData() { if (!chat::hushChatFeatureEnabledAtBuild()) return; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index fa66313..c3b4079 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1818,8 +1818,9 @@ void RenderSettingsPage(App* app) { } else { ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { + // Route through App so the chat session is torn down immediately on lock. s_settingsState.lite_encryption_status = - app->liteWallet()->lockWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed"); + app->lockLiteWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed"); } } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));