// DragonX Wallet - HushChat outgoing memo construction (implementation). #include "chat_outgoing.h" #include "chat_protocol.h" // kHushChat* constants #include namespace dragonx::chat { namespace { // Serialize the HushChat header. nlohmann emits object keys in sorted (alphabetical) order — // cid,e,h,p,t,v,z — which is exactly SilentDragonXLite's on-wire key order. std::string buildHeaderMemo(const std::string& replyZaddr, const std::string& conversationId, const char* type, const std::string& streamHeaderHex, const std::string& publicKeyHex) { nlohmann::json header; header["h"] = 1; // header number (>= 1) header["v"] = kHushChatSupportedVersion; // 0 header["z"] = replyZaddr; // where the peer should reply (my address) header["cid"] = conversationId; header["t"] = type; // "Memo" or "Cont" header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont) header["p"] = publicKeyHex; // my 64-hex crypto_kx public key return header.dump(); } bool present(const std::string& value) { return !value.empty(); } } // namespace ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine, const std::string& myPublicKeyHex, const std::string& myReplyZaddr, const std::string& peerPublicKeyHex, const std::string& peerZaddr, const std::string& conversationId, const std::string& plaintext, OutgoingChatMemos& out) { if (plaintext.empty()) return ChatComposeStatus::EmptyBody; if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) || !present(conversationId)) { return ChatComposeStatus::MissingField; } if (peerPublicKeyHex.size() != kHushChatPublicKeyHexLength) return ChatComposeStatus::BadPeerKey; std::string streamHeaderHex; std::string ciphertextHex; if (encryptOutgoing(mine, peerPublicKeyHex, plaintext, streamHeaderHex, ciphertextHex) != ChatCryptoStatus::Ok) { return ChatComposeStatus::EncryptFailed; } OutgoingChatMemos memos; memos.recipientZaddr = peerZaddr; memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex); memos.payloadMemo = ciphertextHex; if (memos.headerMemo.size() > kHushChatMemoByteLimit || memos.payloadMemo.size() > kHushChatMemoByteLimit) { return ChatComposeStatus::TooLong; } out = std::move(memos); return ChatComposeStatus::Ok; } ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex, const std::string& myReplyZaddr, const std::string& peerZaddr, const std::string& conversationId, const std::string& requestText, OutgoingChatMemos& out) { if (requestText.empty()) return ChatComposeStatus::EmptyBody; // The receive parser treats any memo starting with '{' as a header, so a request payload must // not start with one (see isContactPayloadCandidate in chat_protocol.cpp). if (requestText.front() == '{') return ChatComposeStatus::BadRequestText; if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) || !present(conversationId)) { return ChatComposeStatus::MissingField; } OutgoingChatMemos memos; memos.recipientZaddr = peerZaddr; memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex); memos.payloadMemo = requestText; if (memos.headerMemo.size() > kHushChatMemoByteLimit || memos.payloadMemo.size() > kHushChatMemoByteLimit) { return ChatComposeStatus::TooLong; } out = std::move(memos); return ChatComposeStatus::Ok; } } // namespace dragonx::chat