From d043538e2fcb91590621562939620c983fceef18 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 5 Jul 2026 20:05:26 -0500 Subject: [PATCH] feat(chat): carry decrypt inputs through the memo harvest Phase 1 Step 1 (the linchpin). The receive path had nothing to decrypt: HushChatTransactionMetadata carried only payload_position/size, and the extractor discarded the paired payload memo + the header's "e"/"p". Widen the metadata with sender_public_key_hex ("p"), secretstream_header_hex ("e"), and payload_memo (ciphertext hex for a Message; request text for a ContactRequest), populated from the already-parsed header/payload pair. Add an end-to-end receive-path test: derive two identities, encrypt Alice->Bob, wrap it as a HushChat header+payload memo pair, run it through extractHushChatTransactionMetadata, and decrypt straight from the carried metadata. Also asserts the harvest yields nothing when the feature is disabled. Fields are only populated when featureEnabled (the extractor already returns early otherwise), so an OFF build produces no decrypt material. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/chat/chat_protocol.cpp | 3 +++ src/chat/chat_protocol.h | 5 +++++ tests/test_phase4.cpp | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/chat/chat_protocol.cpp b/src/chat/chat_protocol.cpp index f9db76f..bcda4af 100644 --- a/src/chat/chat_protocol.cpp +++ b/src/chat/chat_protocol.cpp @@ -275,6 +275,9 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata( metadata.header_position = pair.header_position; metadata.payload_position = pair.payload_position; metadata.payload_size = pair.payload_memo.size(); + metadata.sender_public_key_hex = pair.header.public_key_hex; + metadata.secretstream_header_hex = pair.header.secretstream_header_hex; + metadata.payload_memo = pair.payload_memo; result.metadata.push_back(std::move(metadata)); } diff --git a/src/chat/chat_protocol.h b/src/chat/chat_protocol.h index 908241e..4105748 100644 --- a/src/chat/chat_protocol.h +++ b/src/chat/chat_protocol.h @@ -75,6 +75,11 @@ struct HushChatTransactionMetadata { std::size_t header_position = 0; std::size_t payload_position = 0; std::size_t payload_size = 0; + // Decrypt inputs carried through from the paired header + payload memos so the chat + // service can actually decrypt (a Message) or read the request (a ContactRequest). + std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex) + std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest) + std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest) }; struct HushChatTransactionExtractionResult { diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 4812665..3490e64 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -5617,6 +5617,51 @@ void testHushChatCrypto() EXPECT_TRUE(decryptIncoming(bob, ra.public_key_hex, headerHex, "00", plain4) == ChatCryptoStatus::CiphertextTooShort); } +// End-to-end receive path: identity -> encrypt -> HushChat memo pair -> parser harvest +// (extractHushChatTransactionMetadata now carries e/p/ciphertext, Phase 1 Step 1) -> decrypt. +void testHushChatReceivePath() +{ + using namespace dragonx::chat; + + ChatKeyPair alice, bob; + ChatIdentityResult ra = deriveChatIdentityFromSecret("alice-recv", alice, true); + ChatIdentityResult rb = deriveChatIdentityFromSecret("bob-recv", bob, true); + EXPECT_TRUE(ra.status == ChatIdentityStatus::Ready); + EXPECT_TRUE(rb.status == ChatIdentityStatus::Ready); + + // Alice encrypts to Bob, wrapped as a HushChat header memo + payload memo. + std::string e, ct; + EXPECT_TRUE(encryptOutgoing(alice, rb.public_key_hex, "gm bob", e, ct) == ChatCryptoStatus::Ok); + std::string headerMemo = std::string("{\"cid\":\"conv-42\",\"e\":\"") + e + + "\",\"h\":1,\"p\":\"" + ra.public_key_hex + + "\",\"t\":\"Memo\",\"v\":0,\"z\":\"zs-alice-reply\"}"; + + HushChatTransactionInput tx; + tx.txid = "txid-recv-1"; + tx.outputs.push_back(HushChatMemoOutput{0, headerMemo}); + tx.outputs.push_back(HushChatMemoOutput{1, ct}); + + HushChatTransactionExtractionResult ext = extractHushChatTransactionMetadata(tx, true); + EXPECT_TRUE(ext.feature_enabled); + EXPECT_EQ((int)ext.metadata.size(), 1); + const HushChatTransactionMetadata& m = ext.metadata[0]; + EXPECT_TRUE(m.type == HushChatHeaderType::Message); + EXPECT_EQ(m.sender_public_key_hex, ra.public_key_hex); + EXPECT_EQ(m.secretstream_header_hex, e); + EXPECT_EQ(m.payload_memo, ct); + + // Bob decrypts straight from the carried metadata — the whole receive path. + std::string plain; + EXPECT_TRUE(decryptIncoming(bob, m.sender_public_key_hex, m.secretstream_header_hex, + m.payload_memo, plain) == ChatCryptoStatus::Ok); + EXPECT_EQ(plain, std::string("gm bob")); + + // Feature disabled -> the harvest yields nothing. + HushChatTransactionExtractionResult off = extractHushChatTransactionMetadata(tx, false); + EXPECT_TRUE(!off.feature_enabled); + EXPECT_EQ((int)off.metadata.size(), 0); +} + } // namespace int main() @@ -5708,6 +5753,7 @@ int main() testPoolWeightedSelection(); testAtomicFileWrite(); testHushChatCrypto(); + testHushChatReceivePath(); testAddressChecksumValidation(); testLiteServerProbeLive(); testXmrigLiveInstall();