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 "wallet/wallet_capabilities.h"
|
||||||
#include "chat/chat_service.h"
|
#include "chat/chat_service.h"
|
||||||
#include "chat/chat_database.h"
|
#include "chat/chat_database.h"
|
||||||
|
#include "chat/chat_sensitive_vault.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/console_tab.h"
|
#include "ui/windows/console_tab.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -325,6 +326,21 @@ public:
|
|||||||
// hides/restores this conversation. `label` is a plaintext hint (peer address/name) carried on-chain.
|
// 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.
|
// 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);
|
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
|
// 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
|
// (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
|
// 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.
|
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
|
||||||
chat::ChatService chat_service_;
|
chat::ChatService chat_service_;
|
||||||
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
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_provisioned_ = false; // identity set on the service this session
|
||||||
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
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
|
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) {
|
chat_service_.setBlockedPredicate([this](const std::string& cid) {
|
||||||
return settings_ && settings_->isChatBlocked(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)) {
|
if (chat_db_.unlockWithSecret(trimmed)) {
|
||||||
chat_service_.loadFromDatabase();
|
chat_service_.loadFromDatabase();
|
||||||
// Baseline unread: treat everything already in the store at load as read, so only messages
|
// 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=*/"");
|
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)
|
void App::startChatConversation(const std::string& peerZaddr, const std::string& text)
|
||||||
{
|
{
|
||||||
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
|
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user