feat(chat): service-level sensitive gating + local sensitive marks
Wires the sensitive-conversation plumbing into ChatService + Settings: - Settings gains a persisted local sensitive set (isChatSensitive / setChatSensitive / sensitiveChatConversations), mirroring the blocked set, for INSTANT effect the moment the user marks a conversation (before the on-chain signal is harvested back). - ChatService::isConversationSensitive() OR-combines that local mark with the on-chain reduction, and setPersistence() wires it as the DB's per-row key selector (sensitive rows -> vault key). - ingest() withholds a sensitive conversation's messages while the vault key is absent (fail-closed: not shown, not persisted); they re-import + persist under the vault key once it is unlocked and a scan runs. loadFromDatabase() already withholds via the DB's two-key try. Service-tested end to end against a real ChatDatabase (ingest + reload). Phase B, step 4 (service gating). App vault lifecycle + UI next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,10 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
// in the same conversation has a different txid and flows through normally.
|
||||
if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue;
|
||||
if (conversationHiddenOnChain(message.conversation_id)) continue; // hidden by an on-chain delete
|
||||
// Withhold a sensitive conversation's messages while its local vault key is absent (locked vault or
|
||||
// a fresh seed import). Fail-closed: if we can't store it under the vault key we don't surface it.
|
||||
// They re-import and persist under the vault key once it is unlocked and a scan runs.
|
||||
if (isConversationSensitive(message.conversation_id) && !(db_ && db_->hasSensitiveKey())) continue;
|
||||
if (db_ && db_->isTombstoned(message)) continue;
|
||||
|
||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||
@@ -200,6 +204,16 @@ bool ChatService::conversationSensitiveOnChain(const std::string& conversationId
|
||||
auto it = sensitive_control_.find(conversationId);
|
||||
return it != sensitive_control_.end() && it->second.op == "sensitive";
|
||||
}
|
||||
bool ChatService::isConversationSensitive(const std::string& conversationId) const {
|
||||
if (conversationSensitiveOnChain(conversationId)) return true;
|
||||
return local_sensitive_pred_ && local_sensitive_pred_(conversationId);
|
||||
}
|
||||
void ChatService::setPersistence(ChatDatabase* db) {
|
||||
db_ = db;
|
||||
// The DB picks the per-row key by this predicate: a sensitive conversation's rows are stored under the
|
||||
// local vault key. Combines the on-chain reduction with the local (instant) mark.
|
||||
if (db_) db_->setSensitivePredicate([this](const std::string& cid) { return isConversationSensitive(cid); });
|
||||
}
|
||||
std::vector<std::pair<std::string, std::string>> ChatService::onChainSensitiveConversations() const {
|
||||
std::vector<std::pair<std::string, std::string>> out;
|
||||
for (const auto& entry : sensitive_control_) {
|
||||
|
||||
+13
-1
@@ -53,7 +53,8 @@ public:
|
||||
|
||||
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
||||
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
||||
void setPersistence(ChatDatabase* db) { db_ = db; }
|
||||
// Also wires the DB's per-conversation sensitivity predicate to isConversationSensitive().
|
||||
void setPersistence(ChatDatabase* db);
|
||||
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
|
||||
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
|
||||
void loadFromDatabase();
|
||||
@@ -89,6 +90,16 @@ public:
|
||||
// Conversations whose latest on-chain control op is "sensitive", each paired with its control label.
|
||||
std::vector<std::pair<std::string, std::string>> onChainSensitiveConversations() const;
|
||||
|
||||
// A local "sensitive" predicate (typically Settings::isChatSensitive) giving INSTANT effect the moment
|
||||
// the user marks a conversation, before the on-chain signal is harvested back. OR-combined with the
|
||||
// on-chain reduction by isConversationSensitive().
|
||||
void setLocalSensitivePredicate(std::function<bool(const std::string&)> pred) {
|
||||
local_sensitive_pred_ = std::move(pred);
|
||||
}
|
||||
// True if this conversation is sensitive (locally marked OR on-chain). Its rows are stored under the
|
||||
// local vault key; while that key is absent its messages are withheld from the store on ingest/load.
|
||||
bool isConversationSensitive(const std::string& conversationId) const;
|
||||
|
||||
// --- Outgoing (compose) ---
|
||||
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
|
||||
std::string identityPublicKeyHex() const;
|
||||
@@ -131,6 +142,7 @@ private:
|
||||
ChatStore store_;
|
||||
ChatDatabase* db_ = nullptr; // optional; not owned
|
||||
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
|
||||
std::function<bool(const std::string&)> local_sensitive_pred_; // true => cid locally marked sensitive
|
||||
|
||||
// Reduced on-chain control state, cid -> latest signal. Not persisted (re-derived from chain).
|
||||
struct ControlState {
|
||||
|
||||
Reference in New Issue
Block a user