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) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:05:26 -05:00
parent 2502487e44
commit d043538e2f
3 changed files with 54 additions and 0 deletions

View File

@@ -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));
}

View File

@@ -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 {

View File

@@ -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();