feat(chat): per-conversation local-key encryption in ChatDatabase
ChatDatabase gains a second AEAD key — the local vault key (setSensitiveKey / lockSensitive) — and a sensitivity predicate. Rows of conversations the predicate marks sensitive are encrypted under the vault key instead of the seed-derived key, and are persisted only while the vault is unlocked (never stored under the seed key). load() tries the seed key then the vault key, so a sensitive row is WITHHELD whenever the vault key is absent — including on a fresh instance that holds only the seed (the "fresh seed import can't read sensitive conversations" guarantee); a wrong vault key withholds too, never yields garbage. encrypt/decrypt are now key-parametrized; lock() also wipes the vault key. Unit-tested end to end. Phase B, step 3 (DB dual-key). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -40,6 +41,17 @@ public:
|
||||
void lock(); // wipe the key material (DB handle stays open); load()/append() then no-op
|
||||
bool hasKey() const { return key_ready_; }
|
||||
|
||||
// --- Sensitive conversations (per-conversation local-key encryption) ---
|
||||
// Provide the local vault key (from ChatSensitiveVault). Rows of conversations the sensitive
|
||||
// predicate marks sensitive are AEAD-encrypted under THIS key instead of the seed-derived key, so
|
||||
// they can't be read without it (even by the seed). The caller still owns and should wipe `key`.
|
||||
void setSensitiveKey(const std::array<unsigned char, 32>& key);
|
||||
void lockSensitive(); // wipe the sensitive key; sensitive rows then withheld
|
||||
bool hasSensitiveKey() const { return sensitive_ready_; }
|
||||
// Predicate: true for a conversation id that should be stored under the sensitive key. Consulted on
|
||||
// WRITE to pick the key; on READ both keys are tried, so a predicate isn't needed to decrypt.
|
||||
void setSensitivePredicate(std::function<bool(const std::string&)> pred) { sensitive_pred_ = std::move(pred); }
|
||||
|
||||
// Persist one message (INSERT OR IGNORE, deduped by a keyed hash of txid+payload_position).
|
||||
// Returns true if newly inserted; false on duplicate or while locked.
|
||||
bool append(const ChatMessage& message);
|
||||
@@ -80,12 +92,18 @@ private:
|
||||
std::string dedupHash(const std::string& txid, std::size_t position) const;
|
||||
std::string serialize(const ChatMessage& message) const;
|
||||
bool deserialize(const std::string& json, ChatMessage& out) const;
|
||||
bool encrypt(const std::string& plain,
|
||||
bool encrypt(const unsigned char* key,
|
||||
const std::string& plain,
|
||||
std::vector<unsigned char>& nonce,
|
||||
std::vector<unsigned char>& cipher) const;
|
||||
bool decrypt(const std::vector<unsigned char>& nonce,
|
||||
bool decrypt(const unsigned char* key,
|
||||
const std::vector<unsigned char>& nonce,
|
||||
const std::vector<unsigned char>& cipher,
|
||||
std::string& plain) const;
|
||||
// The key a row for `cid` is written under. Returns nullptr and sets skip=true when the conversation
|
||||
// is sensitive but the vault is locked — the row must NOT be persisted (under the seed key it would be
|
||||
// readable without the vault, defeating the point); it re-imports from chain once the vault is unlocked.
|
||||
const unsigned char* writeKeyFor(const std::string& cid, bool& skip) const;
|
||||
void close();
|
||||
|
||||
sqlite3* db_ = nullptr;
|
||||
@@ -93,6 +111,9 @@ private:
|
||||
std::array<unsigned char, 32> key_{}; // AEAD storage key (seed-derived)
|
||||
std::string wallet_tag_; // seed-derived row partition (a keyed hash, hex)
|
||||
bool key_ready_ = false;
|
||||
std::array<unsigned char, 32> sensitive_key_{}; // local vault key for sensitive conversations' rows
|
||||
bool sensitive_ready_ = false;
|
||||
std::function<bool(const std::string&)> sensitive_pred_; // true => store this cid under the sensitive key
|
||||
std::unordered_set<std::string> tombstones_; // dedup_hash cache of locally-deleted messages
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user