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:
@@ -15382,6 +15382,66 @@ void testChatSensitiveDatabase()
|
||||
scrub(dbPath);
|
||||
}
|
||||
|
||||
// Service-level sensitive gating: a sensitive conversation's messages are withheld on ingest and on
|
||||
// reload while the DB vault key is absent, and flow + persist once it is present.
|
||||
void testHushChatSensitiveService()
|
||||
{
|
||||
using namespace dragonx::chat;
|
||||
namespace fs = std::filesystem;
|
||||
const std::string dbPath = (fs::temp_directory_path() / "drgx_chat_senssvc_test.sqlite").string();
|
||||
auto scrub = [](const std::string& p) { fs::remove(p); fs::remove(p + "-wal"); fs::remove(p + "-shm"); };
|
||||
scrub(dbPath);
|
||||
|
||||
ChatKeyPair me, peer;
|
||||
ChatIdentityResult rm = deriveChatIdentityFromSecret("senssvc-me", me, true);
|
||||
ChatIdentityResult rp = deriveChatIdentityFromSecret("senssvc-peer", peer, true);
|
||||
EXPECT_TRUE(rm.status == ChatIdentityStatus::Ready && rp.status == ChatIdentityStatus::Ready);
|
||||
|
||||
ChatDatabase db(dbPath);
|
||||
EXPECT_TRUE(db.unlockWithSecret("seed-xyz"));
|
||||
ChatService svc; svc.setIdentity(me); svc.setPersistence(&db);
|
||||
svc.setLocalSensitivePredicate([](const std::string& cid) { return cid == "sens"; });
|
||||
EXPECT_TRUE(svc.isConversationSensitive("sens"));
|
||||
EXPECT_TRUE(!svc.isConversationSensitive("normal"));
|
||||
|
||||
auto feedPeerMsg = [&](const std::string& cid, const std::string& txid, const std::string& body) {
|
||||
OutgoingChatMemos m;
|
||||
EXPECT_TRUE(buildOutgoingMessage(peer, rp.public_key_hex, "zs-peer", rm.public_key_hex, "zs-me",
|
||||
cid, body, m) == ChatComposeStatus::Ok);
|
||||
HushChatTransactionInput tx; tx.txid = txid;
|
||||
tx.outputs.push_back({0, m.headerMemo}); tx.outputs.push_back({1, m.payloadMemo});
|
||||
auto ex = extractHushChatTransactionMetadata(tx, true);
|
||||
const std::int64_t t = static_cast<std::int64_t>(std::time(nullptr));
|
||||
return svc.ingest(ex.metadata, {{txid, t}}, t);
|
||||
};
|
||||
|
||||
// Vault locked: a sensitive message is withheld (fail-closed); a normal one flows.
|
||||
EXPECT_EQ(feedPeerMsg("normal", "n1", "hi"), 1);
|
||||
EXPECT_EQ(feedPeerMsg("sens", "s1", "secret"), 0);
|
||||
EXPECT_EQ((int)svc.store().conversation("sens").size(), 0);
|
||||
EXPECT_EQ((int)svc.store().conversation("normal").size(), 1);
|
||||
|
||||
// Unlock the vault (provide a key): the sensitive message now flows and persists.
|
||||
std::array<unsigned char, 32> vkey; vkey.fill(0x33);
|
||||
db.setSensitiveKey(vkey);
|
||||
EXPECT_EQ(feedPeerMsg("sens", "s1", "secret"), 1);
|
||||
EXPECT_EQ((int)svc.store().conversation("sens").size(), 1);
|
||||
|
||||
// Reload from the DB with the vault LOCKED: sensitive withheld, normal loads.
|
||||
db.lockSensitive();
|
||||
svc.store().clear();
|
||||
svc.loadFromDatabase();
|
||||
EXPECT_EQ((int)svc.store().conversation("sens").size(), 0);
|
||||
EXPECT_EQ((int)svc.store().conversation("normal").size(), 1);
|
||||
|
||||
// Reload with the vault key present: the sensitive thread reappears.
|
||||
db.setSensitiveKey(vkey);
|
||||
svc.store().clear();
|
||||
svc.loadFromDatabase();
|
||||
EXPECT_EQ((int)svc.store().conversation("sens").size(), 1);
|
||||
scrub(dbPath);
|
||||
}
|
||||
|
||||
void testHushChatDatabase()
|
||||
{
|
||||
using namespace dragonx::chat;
|
||||
@@ -20238,6 +20298,7 @@ int main()
|
||||
testHushChatService();
|
||||
testHushChatDatabase();
|
||||
testChatSensitiveDatabase();
|
||||
testHushChatSensitiveService();
|
||||
testHushChatDeleteConversation();
|
||||
testChatStoreTxidIndex();
|
||||
testChatStoreLatestAndMove();
|
||||
|
||||
Reference in New Issue
Block a user