feat(chat): App vault lifecycle for sensitive conversations
Adds the App-level glue that makes sensitive mode operable: - Owns ChatSensitiveVault (path <configDir>/chat_sensitive.vault); provisioning wires the service's local sensitive predicate to Settings::isChatSensitive and points the vault at its path (left LOCKED until the user enters its passphrase). - chatSensitiveVaultState() (NotConfigured/Locked/Unlocked) + create / unlock / lock: unlock pushes the key to chat_db_ and pulls sensitive rows into the store (+ re-harvest); lock drops the key and reloads so sensitive threads vanish. - markChatConversationSensitive / unmark: migrate the thread's existing rows to/from the vault key via upsert (so the whole thread hides when locked) and broadcast the on-chain "sensitive"/"unsensitive" signal via sendChatControl. (wallet lock already wipes the vault key via chat_db_.lock().) Phase B, step 5a (App lifecycle). UI next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
#include "wallet/wallet_capabilities.h"
|
||||
#include "chat/chat_service.h"
|
||||
#include "chat/chat_database.h"
|
||||
#include "chat/chat_sensitive_vault.h"
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/console_tab.h"
|
||||
#include "imgui.h"
|
||||
@@ -325,6 +326,21 @@ public:
|
||||
// hides/restores this conversation. `label` is a plaintext hint (peer address/name) carried on-chain.
|
||||
// Reuses the chat send coordinator + the 0-value kChatMinFeeDrgx z_sendmany path; no UI echo bubble.
|
||||
void sendChatControl(const std::string& conversationId, const std::string& op, const std::string& label);
|
||||
|
||||
// --- Sensitive conversations (per-conversation local-key encryption) ---
|
||||
enum class ChatVaultState { NotConfigured, Locked, Unlocked };
|
||||
ChatVaultState chatSensitiveVaultState() const;
|
||||
bool chatSensitiveVaultConfigured() const; // a vault file exists (regardless of lock state)
|
||||
// Create the vault (first-time setup) and unlock it. Returns false if one already exists or on error.
|
||||
bool createChatSensitiveVault(const std::string& passphrase);
|
||||
// Provide the vault key to the chat DB so sensitive rows decrypt; also pulls them into the store.
|
||||
bool unlockChatSensitiveVault(const std::string& passphrase);
|
||||
void lockChatSensitiveVault(); // drop the key; sensitive threads vanish from the view
|
||||
// Mark/unmark a conversation sensitive. Requires the vault UNLOCKED (migrates its rows to/from the
|
||||
// vault key) and broadcasts the on-chain "sensitive"/"unsensitive" signal. `peerAddr` labels it.
|
||||
bool markChatConversationSensitive(const std::string& cid, const std::string& peerAddr);
|
||||
void unmarkChatConversationSensitive(const std::string& cid);
|
||||
|
||||
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
||||
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
||||
// the chat feature is off. Returns true when it had to fabricate the (random) demo identity
|
||||
@@ -1024,6 +1040,8 @@ private:
|
||||
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
|
||||
chat::ChatService chat_service_;
|
||||
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
||||
chat::ChatSensitiveVault chat_sensitive_vault_; // local key vault for "sensitive" conversations
|
||||
std::string chatSensitiveVaultPath() const; // <configDir>/chat_sensitive.vault
|
||||
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
||||
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
||||
std::chrono::steady_clock::time_point chat_identity_retry_at_{}; // after a transient failure
|
||||
|
||||
@@ -4217,6 +4217,13 @@ void App::provisionChatIdentityFromSecret(std::string secret)
|
||||
chat_service_.setBlockedPredicate([this](const std::string& cid) {
|
||||
return settings_ && settings_->isChatBlocked(cid);
|
||||
});
|
||||
// Sensitive conversations: the local mark gives instant effect; the DB routes those rows to the
|
||||
// vault key. The vault itself stays LOCKED until the user enters its passphrase (the DB then
|
||||
// withholds sensitive rows), so provisioning never auto-unlocks it.
|
||||
chat_service_.setLocalSensitivePredicate([this](const std::string& cid) {
|
||||
return settings_ && settings_->isChatSensitive(cid);
|
||||
});
|
||||
chat_sensitive_vault_.setPath(chatSensitiveVaultPath());
|
||||
if (chat_db_.unlockWithSecret(trimmed)) {
|
||||
chat_service_.loadFromDatabase();
|
||||
// Baseline unread: treat everything already in the store at load as read, so only messages
|
||||
@@ -4704,6 +4711,80 @@ void App::sendChatControl(const std::string& conversationId, const std::string&
|
||||
enqueueChatSend(LiteOpKind::ChatSend, memos, /*echoLocalId=*/"");
|
||||
}
|
||||
|
||||
std::string App::chatSensitiveVaultPath() const
|
||||
{
|
||||
return (std::filesystem::path(util::Platform::getConfigDir()) / "chat_sensitive.vault").string();
|
||||
}
|
||||
|
||||
bool App::chatSensitiveVaultConfigured() const
|
||||
{
|
||||
chat::ChatSensitiveVault probe(chatSensitiveVaultPath());
|
||||
return probe.exists();
|
||||
}
|
||||
|
||||
App::ChatVaultState App::chatSensitiveVaultState() const
|
||||
{
|
||||
if (!chatSensitiveVaultConfigured()) return ChatVaultState::NotConfigured;
|
||||
return chat_db_.hasSensitiveKey() ? ChatVaultState::Unlocked : ChatVaultState::Locked;
|
||||
}
|
||||
|
||||
bool App::createChatSensitiveVault(const std::string& passphrase)
|
||||
{
|
||||
chat_sensitive_vault_.setPath(chatSensitiveVaultPath());
|
||||
std::array<unsigned char, 32> key{};
|
||||
const bool ok = chat_sensitive_vault_.create(passphrase, key);
|
||||
if (ok) chat_db_.setSensitiveKey(key); // the DB now encrypts sensitive rows under this key
|
||||
sodium_memzero(key.data(), key.size());
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool App::unlockChatSensitiveVault(const std::string& passphrase)
|
||||
{
|
||||
chat_sensitive_vault_.setPath(chatSensitiveVaultPath());
|
||||
std::array<unsigned char, 32> key{};
|
||||
const bool ok = chat_sensitive_vault_.unlock(passphrase, key);
|
||||
if (ok) {
|
||||
chat_db_.setSensitiveKey(key);
|
||||
chat_service_.loadFromDatabase(); // sensitive rows now decrypt → pull them into the store
|
||||
chat_note_rescan_requested_ = true; // re-harvest any that were withheld while the vault was locked
|
||||
}
|
||||
sodium_memzero(key.data(), key.size());
|
||||
return ok;
|
||||
}
|
||||
|
||||
void App::lockChatSensitiveVault()
|
||||
{
|
||||
chat_db_.lockSensitive();
|
||||
// Drop the now-undecryptable sensitive threads from the view at once: reload (the DB withholds any
|
||||
// row it can't decrypt without the vault key), keeping the in-memory store == what is readable.
|
||||
chat_service_.store().clear();
|
||||
chat_service_.loadFromDatabase();
|
||||
}
|
||||
|
||||
bool App::markChatConversationSensitive(const std::string& cid, const std::string& peerAddr)
|
||||
{
|
||||
if (cid.empty() || chatSensitiveVaultState() != ChatVaultState::Unlocked) return false;
|
||||
if (settings_) { settings_->setChatSensitive(cid, peerAddr, true); settings_->save(); }
|
||||
// Migrate this conversation's EXISTING rows from the seed key to the vault key: the predicate is now
|
||||
// true, so upsert() re-encrypts each under the vault key — so the WHOLE thread hides when the vault
|
||||
// locks (new messages already go to the vault key). Incoming history stays re-harvestable from chain
|
||||
// (the honest caveat the UI warns about); this protects the local at-rest copy.
|
||||
for (const auto& m : chat_service_.store().conversation(cid)) chat_db_.upsert(m);
|
||||
sendChatControl(cid, "sensitive", peerAddr.empty() ? cid : peerAddr); // signal other devices / fresh import
|
||||
return true;
|
||||
}
|
||||
|
||||
void App::unmarkChatConversationSensitive(const std::string& cid)
|
||||
{
|
||||
if (cid.empty()) return;
|
||||
if (settings_) { settings_->setChatSensitive(cid, "", false); settings_->save(); }
|
||||
// Migrate rows back to the seed key (predicate now false). Needs the vault unlocked so the rows are in
|
||||
// the store to re-write; if it is locked there is nothing to migrate (they re-key on the next scan).
|
||||
if (chat_db_.hasSensitiveKey())
|
||||
for (const auto& m : chat_service_.store().conversation(cid)) chat_db_.upsert(m);
|
||||
sendChatControl(cid, "unsensitive", cid);
|
||||
}
|
||||
|
||||
void App::startChatConversation(const std::string& peerZaddr, const std::string& text)
|
||||
{
|
||||
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
|
||||
|
||||
Reference in New Issue
Block a user