Swap the in-memory-only chat store for durable sqlite persistence with real
per-transaction timestamps, encrypted at rest under a key derived from the
wallet's own seed (no passphrase, works on encrypted and unencrypted wallets).
- ChatDatabase (src/chat/chat_database.{h,cpp}): sqlite store mirroring
data::TransactionHistoryCache. unlockWithSecret(seed) derives a 32-byte AEAD
storage key and a wallet-partition tag via domain-separated keyed BLAKE2b
(generichash) contexts. Every record — bodies, peer z-addrs, threading,
timestamps — is crypto_aead_xchacha20poly1305_ietf-encrypted with a random
nonce and the wallet tag as associated data; even the dedup key is a keyed
hash of txid+position, so nothing about your conversations is in cleartext on
disk. Rows are partitioned per-wallet; a different seed sees nothing. Messages
are decrypted once at ingest then re-encrypted under the storage key, so
load() needs only the storage key, not the chat identity.
- ChatService: ingest() now stamps each message with its own transaction time
(txid->time map + fallback) and writes new (store-deduped) messages through to
the database; loadFromDatabase() rehydrates the in-memory read model on unlock.
- App: unlock the chat DB with the same seed in provisionChatIdentityFromSecret
and load prior history; lock the DB + clear the decrypted in-memory store on
relock and on lite-controller rebuild.
Adversarial review (4 confirmed findings, all fixed): don't provision if the
wallet locks mid-fetch (re-check isLocked at completion); wipe the serialized
plaintext temporary in append(); trim the seed into a separate fully-wiped
buffer (no residue past a shrunk size()); scrub the mnemonic copy in the RPC
json result.
Tests: ChatDatabase round-trip (persist/reload, field + order fidelity), dedup,
per-wallet isolation, lock inertness, and ChatService write-through + reload
without an identity. Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified:
Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean; caches
restored to the OFF default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.4 KiB
C++
74 lines
2.4 KiB
C++
// DragonX Wallet - HushChat service (implementation).
|
|
|
|
#include "chat_service.h"
|
|
|
|
#include "chat_database.h"
|
|
|
|
#include <utility>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
ChatService::~ChatService() {
|
|
clearIdentity();
|
|
}
|
|
|
|
void ChatService::setIdentity(const ChatKeyPair& keys) {
|
|
identity_ = keys;
|
|
has_identity_ = true;
|
|
}
|
|
|
|
void ChatService::clearIdentity() {
|
|
wipeChatKeyPair(identity_);
|
|
has_identity_ = false;
|
|
}
|
|
|
|
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
|
std::int64_t fallbackTimestamp) {
|
|
if (!has_identity_) return 0;
|
|
|
|
int added = 0;
|
|
for (const auto& meta : metadata) {
|
|
ChatMessage message;
|
|
message.direction = ChatDirection::Incoming;
|
|
message.txid = meta.txid;
|
|
message.conversation_id = meta.conversation_id;
|
|
message.peer_zaddr = meta.reply_zaddr;
|
|
message.peer_public_key_hex = meta.sender_public_key_hex;
|
|
const auto timeIt = txTimestamps.find(meta.txid);
|
|
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
|
message.payload_position = meta.payload_position;
|
|
|
|
if (meta.type == HushChatHeaderType::ContactRequest) {
|
|
message.kind = ChatMessageKind::ContactRequest;
|
|
message.body = meta.payload_memo; // plaintext request text
|
|
} else {
|
|
message.kind = ChatMessageKind::Message;
|
|
std::string plaintext;
|
|
const ChatCryptoStatus status = decryptIncoming(
|
|
identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
|
|
meta.payload_memo, plaintext);
|
|
if (status != ChatCryptoStatus::Ok) continue; // drop undecryptable silently
|
|
message.body = std::move(plaintext);
|
|
}
|
|
|
|
// In-memory store dedups (txid+position); only persist the genuinely new ones. On the next
|
|
// session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
|
|
// the store dedup prevents a duplicate write.
|
|
if (store_.append(message)) {
|
|
if (db_) db_->append(message);
|
|
++added;
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
void ChatService::loadFromDatabase() {
|
|
if (!db_) return;
|
|
for (const auto& message : db_->load()) {
|
|
store_.append(message);
|
|
}
|
|
}
|
|
|
|
} // namespace dragonx::chat
|