#include "chat_protocol.h" #include #include #include #include #include namespace dragonx::chat { namespace { bool isHexString(const std::string& value) { for (unsigned char ch : value) { if (!std::isxdigit(ch)) return false; } return true; } bool isCiphertextPayloadCandidate(const std::string& value) { return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.size() % 2 == 0 && isHexString(value); } bool isContactPayloadCandidate(const std::string& value) { return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.front() != '{'; } bool readRequiredString(const nlohmann::json& object, const char* key, std::string& value, std::string& error) { auto it = object.find(key); if (it == object.end()) { error = std::string("missing field: ") + key; return false; } if (!it->is_string()) { error = std::string("field is not a string: ") + key; return false; } value = it->get(); return true; } bool readRequiredInt(const nlohmann::json& object, const char* key, int& value, std::string& error) { auto it = object.find(key); if (it == object.end()) { error = std::string("missing field: ") + key; return false; } if (!it->is_number_integer()) { error = std::string("field is not an integer: ") + key; return false; } value = it->get(); return true; } HushChatHeaderParseResult fail(std::string error) { HushChatHeaderParseResult result; result.error = std::move(error); return result; } } // namespace const char* hushChatHeaderTypeName(HushChatHeaderType type) { switch (type) { case HushChatHeaderType::Message: return "Memo"; case HushChatHeaderType::ContactRequest: return "Cont"; } return "Unknown"; } const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue) { switch (issue) { case HushChatMemoGroupingIssue::InvalidHeader: return "InvalidHeader"; case HushChatMemoGroupingIssue::MissingPayload: return "MissingPayload"; case HushChatMemoGroupingIssue::DuplicateHeader: return "DuplicateHeader"; case HushChatMemoGroupingIssue::OversizedMemo: return "OversizedMemo"; } return "Unknown"; } HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo) { if (memo.empty()) return fail("empty memo"); if (memo.size() > kHushChatMemoByteLimit) return fail("memo exceeds HushChat memo byte limit"); if (memo.front() != '{') return fail("memo is not a HushChat header JSON object"); nlohmann::json object; try { object = nlohmann::json::parse(memo); } catch (const nlohmann::json::parse_error& e) { return fail(std::string("invalid JSON: ") + e.what()); } if (!object.is_object()) return fail("header memo JSON is not an object"); HushChatHeader header; std::string type; std::string error; if (!readRequiredInt(object, "h", header.header_number, error)) return fail(error); if (!readRequiredInt(object, "v", header.version, error)) return fail(error); if (!readRequiredString(object, "z", header.reply_zaddr, error)) return fail(error); if (!readRequiredString(object, "cid", header.conversation_id, error)) return fail(error); if (!readRequiredString(object, "t", type, error)) return fail(error); if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error); if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error); if (header.header_number < 1) return fail("header number must be positive"); if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version"); if (header.reply_zaddr.empty()) return fail("reply z-address is empty"); if (header.conversation_id.empty()) return fail("conversation id is empty"); if (type == "Memo") { header.type = HushChatHeaderType::Message; } else if (type == "Cont") { header.type = HushChatHeaderType::ContactRequest; } else { return fail("unknown HushChat header type"); } if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) { return fail("public key must be 32 bytes encoded as hex"); } if (header.type == HushChatHeaderType::Message) { if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength || !isHexString(header.secretstream_header_hex)) { return fail("message header must include a 24 byte secretstream header encoded as hex"); } } else if (!header.secretstream_header_hex.empty()) { return fail("contact request header must not include a secretstream header"); } HushChatHeaderParseResult result; result.ok = true; result.header = std::move(header); return result; } HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector& outputs) { struct OrderedOutput { std::size_t input_index = 0; HushChatMemoOutput output; }; std::vector ordered; ordered.reserve(outputs.size()); for (std::size_t index = 0; index < outputs.size(); ++index) { ordered.push_back(OrderedOutput{index, outputs[index]}); } std::stable_sort(ordered.begin(), ordered.end(), [](const OrderedOutput& left, const OrderedOutput& right) { if (left.output.position == right.output.position) return left.input_index < right.input_index; return left.output.position < right.output.position; }); HushChatMemoGroupingResult result; std::optional pending_header_output; std::optional pending_header; auto addIssue = [&](HushChatMemoGroupingIssue issue, std::size_t position, std::string detail) { result.issues.push_back(HushChatMemoGroupingIssueInfo{issue, position, std::move(detail)}); }; auto clearPendingAsMissing = [&]() { if (!pending_header_output) return; addIssue(HushChatMemoGroupingIssue::MissingPayload, pending_header_output->position, "header did not have a matching payload memo"); pending_header_output.reset(); pending_header.reset(); }; for (const auto& entry : ordered) { const auto& output = entry.output; if (output.memo.size() > kHushChatMemoByteLimit) { addIssue(HushChatMemoGroupingIssue::OversizedMemo, output.position, "memo exceeds HushChat memo byte limit"); continue; } if (!output.memo.empty() && output.memo.front() == '{') { auto parsed = parseHushChatHeaderMemo(output.memo); if (!parsed.ok) { addIssue(HushChatMemoGroupingIssue::InvalidHeader, output.position, parsed.error); continue; } if (pending_header_output) { addIssue(HushChatMemoGroupingIssue::DuplicateHeader, output.position, "encountered another HushChat header before a payload"); clearPendingAsMissing(); } pending_header_output = output; pending_header = std::move(parsed.header); continue; } if (!pending_header_output || !pending_header) { ++result.ignored_memo_count; continue; } const bool payload_matches = pending_header->type == HushChatHeaderType::Message ? isCiphertextPayloadCandidate(output.memo) : isContactPayloadCandidate(output.memo); if (!payload_matches) { ++result.ignored_memo_count; continue; } HushChatMemoPair pair; pair.header = std::move(*pending_header); pair.header_position = pending_header_output->position; pair.payload_position = output.position; pair.payload_memo = output.memo; result.pairs.push_back(std::move(pair)); pending_header_output.reset(); pending_header.reset(); } clearPendingAsMissing(); return result; } HushChatTransactionExtractionResult extractHushChatTransactionMetadata( const HushChatTransactionInput& transaction, bool featureEnabled) { HushChatTransactionExtractionResult result; result.feature_enabled = featureEnabled; if (!featureEnabled || transaction.txid.empty()) return result; auto grouped = groupHushChatMemoOutputs(transaction.outputs); result.ignored_memo_count = grouped.ignored_memo_count; result.issues.reserve(grouped.issues.size()); for (const auto& issue : grouped.issues) { result.issues.push_back(HushChatMemoGroupingIssueInfo{ issue.issue, issue.position, hushChatMemoGroupingIssueName(issue.issue) }); } result.metadata.reserve(grouped.pairs.size()); for (const auto& pair : grouped.pairs) { HushChatTransactionMetadata metadata; metadata.txid = transaction.txid; metadata.type = pair.header.type; metadata.conversation_id = pair.header.conversation_id; metadata.reply_zaddr = pair.header.reply_zaddr; 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)); } return result; } } // namespace dragonx::chat