Files
ObsidianDragon/src/chat/chat_protocol.cpp
DanS 863d015628 feat(lite): lite wallet foundation (inherited working-tree state)
Preserve the previously-uncommitted lite wallet implementation and related dev WIP
under version control:
- src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle,
  sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh
  services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes:
  lifecycle walletReady now parses the response; default chain name -> "main".)
- src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp;
  scripts/build-lite-backend-artifact.sh.
- Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar,
  mining_tab, bootstrap dialog, and version headers captured as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:15:28 -05:00

1855 lines
78 KiB
C++

#include "chat_protocol.h"
#include <nlohmann/json.hpp>
#include <sodium.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <fstream>
#include <optional>
#include <sstream>
#include <utility>
namespace dragonx::chat {
namespace {
bool isHexString(const std::string& value)
{
for (unsigned char ch : value) {
if (!std::isxdigit(ch)) return false;
}
return true;
}
std::optional<unsigned char> hexNibble(unsigned char character)
{
if (character >= '0' && character <= '9') return static_cast<unsigned char>(character - '0');
if (character >= 'a' && character <= 'f') return static_cast<unsigned char>(character - 'a' + 10);
if (character >= 'A' && character <= 'F') return static_cast<unsigned char>(character - 'A' + 10);
return std::nullopt;
}
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<std::string>();
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<int>();
return true;
}
HushChatHeaderParseResult fail(std::string error)
{
HushChatHeaderParseResult result;
result.error = std::move(error);
return result;
}
std::optional<std::string> readJsonString(const nlohmann::json& object, const char* key)
{
auto it = object.find(key);
if (it == object.end() || !it->is_string()) return std::nullopt;
return it->get<std::string>();
}
std::optional<std::size_t> readJsonSize(const nlohmann::json& object, const char* key)
{
auto it = object.find(key);
if (it == object.end() || (!it->is_number_integer() && !it->is_number_unsigned())) return std::nullopt;
if (it->is_number_unsigned()) return it->get<std::size_t>();
auto value = it->get<long long>();
if (value < 0) return std::nullopt;
return static_cast<std::size_t>(value);
}
std::optional<HushChatDecryptDirection> parseFixtureDirection(const std::string& value)
{
if (value == "Incoming") return HushChatDecryptDirection::Incoming;
if (value == "Outgoing") return HushChatDecryptDirection::Outgoing;
return std::nullopt;
}
std::optional<HushChatSessionKeySelection> parseFixtureSessionKeySelection(const std::string& value)
{
if (value == "ClientRx") return HushChatSessionKeySelection::ClientRx;
if (value == "ServerTx") return HushChatSessionKeySelection::ServerTx;
return std::nullopt;
}
std::optional<HushChatCompatibilityFixtureKind> parseFixtureKind(const std::string& value)
{
if (value == "incoming_memo") return HushChatCompatibilityFixtureKind::IncomingMemo;
if (value == "outgoing_memo") return HushChatCompatibilityFixtureKind::OutgoingMemo;
if (value == "seed_public_key_projection") return HushChatCompatibilityFixtureKind::SeedPublicKeyProjection;
if (value == "corrupted_auth_failure") return HushChatCompatibilityFixtureKind::CorruptedAuthFailure;
if (value == "cont_exclusion") return HushChatCompatibilityFixtureKind::ContactExclusion;
return std::nullopt;
}
std::optional<HushChatCompatibilityFixtureFileStatus> parseFixtureFileStatus(const std::string& value)
{
if (value == "pending") return HushChatCompatibilityFixtureFileStatus::Pending;
if (value == "ready") return HushChatCompatibilityFixtureFileStatus::Ready;
return std::nullopt;
}
std::string lowerCopy(const std::string& value)
{
std::string lowered;
lowered.reserve(value.size());
for (unsigned char ch : value) lowered.push_back(static_cast<char>(std::tolower(ch)));
return lowered;
}
bool isProhibitedCaptureManifestKey(const std::string& key)
{
const std::string lowered = lowerCopy(key);
if (lowered.rfind("no_", 0) == 0) return false;
static const std::vector<std::string> prohibitedKeys = {
"fixture",
"fixtures",
"passphrase",
"password",
"plaintext",
"plaintext_hash_hex",
"memo",
"memostr",
"memo_contents",
"header_memo",
"ciphertext",
"ciphertext_memo",
"ciphertext_bytes",
"private_key",
"viewing_key",
"spending_key",
"wallet_seed",
"wallet_file",
"wallet_files",
"stored_chat_key_hex",
"local_public_key_hex",
"peer_public_key_hex",
"public_key_hex",
"secretstream_header",
"derived_key",
"session_key",
"secret_key",
"seed_bytes"
};
return std::find(prohibitedKeys.begin(), prohibitedKeys.end(), lowered) != prohibitedKeys.end();
}
std::size_t countProhibitedCaptureManifestFields(const nlohmann::json& value)
{
std::size_t count = 0;
if (value.is_object()) {
for (auto it = value.begin(); it != value.end(); ++it) {
if (isProhibitedCaptureManifestKey(it.key())) ++count;
count += countProhibitedCaptureManifestFields(it.value());
}
} else if (value.is_array()) {
for (const auto& item : value) count += countProhibitedCaptureManifestFields(item);
}
return count;
}
std::vector<const char*> requiredCaptureHandlingFlags()
{
return {
"disposable_wallets_only",
"non_sensitive_vectors_only",
"no_passphrases",
"no_plaintext",
"no_memo_contents",
"no_private_keys",
"no_wallet_files",
"no_ciphertext_byte_dumps",
"no_derived_keys",
"no_session_keys",
"redacted_report_only"
};
}
} // 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";
}
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error)
{
switch (error) {
case HushChatDecryptPreflightError::None:
return "None";
case HushChatDecryptPreflightError::FeatureDisabled:
return "FeatureDisabled";
case HushChatDecryptPreflightError::NonMessageHeader:
return "NonMessageHeader";
case HushChatDecryptPreflightError::InvalidHeaderNumber:
return "InvalidHeaderNumber";
case HushChatDecryptPreflightError::UnsupportedVersion:
return "UnsupportedVersion";
case HushChatDecryptPreflightError::MissingReplyAddress:
return "MissingReplyAddress";
case HushChatDecryptPreflightError::MissingConversationId:
return "MissingConversationId";
case HushChatDecryptPreflightError::InvalidSecretstreamHeader:
return "InvalidSecretstreamHeader";
case HushChatDecryptPreflightError::InvalidPublicKey:
return "InvalidPublicKey";
case HushChatDecryptPreflightError::EmptyCiphertext:
return "EmptyCiphertext";
case HushChatDecryptPreflightError::OversizedCiphertext:
return "OversizedCiphertext";
case HushChatDecryptPreflightError::OddLengthCiphertext:
return "OddLengthCiphertext";
case HushChatDecryptPreflightError::InvalidCiphertextHex:
return "InvalidCiphertextHex";
case HushChatDecryptPreflightError::TruncatedCiphertext:
return "TruncatedCiphertext";
}
return "Unknown";
}
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error)
{
switch (error) {
case HushChatHexDecodeError::None:
return "None";
case HushChatHexDecodeError::Empty:
return "Empty";
case HushChatHexDecodeError::OddLength:
return "OddLength";
case HushChatHexDecodeError::InvalidHex:
return "InvalidHex";
case HushChatHexDecodeError::UnexpectedByteLength:
return "UnexpectedByteLength";
}
return "Unknown";
}
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction)
{
switch (direction) {
case HushChatDecryptDirection::Incoming:
return "Incoming";
case HushChatDecryptDirection::Outgoing:
return "Outgoing";
}
return "Unknown";
}
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection)
{
switch (selection) {
case HushChatSessionKeySelection::ClientRx:
return "ClientRx";
case HushChatSessionKeySelection::ServerTx:
return "ServerTx";
}
return "Unknown";
}
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error)
{
switch (error) {
case HushChatDecryptInputError::None:
return "None";
case HushChatDecryptInputError::FeatureDisabled:
return "FeatureDisabled";
case HushChatDecryptInputError::InvalidStoredChatKey:
return "InvalidStoredChatKey";
case HushChatDecryptInputError::DecryptPreflightFailed:
return "DecryptPreflightFailed";
case HushChatDecryptInputError::InvalidPeerPublicKey:
return "InvalidPeerPublicKey";
case HushChatDecryptInputError::InvalidStreamHeader:
return "InvalidStreamHeader";
case HushChatDecryptInputError::InvalidCiphertext:
return "InvalidCiphertext";
}
return "Unknown";
}
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error)
{
switch (error) {
case HushChatCompatibilityFixtureError::None:
return "None";
case HushChatCompatibilityFixtureError::FeatureDisabled:
return "FeatureDisabled";
case HushChatCompatibilityFixtureError::MissingFixtureId:
return "MissingFixtureId";
case HushChatCompatibilityFixtureError::InvalidLocalPublicKey:
return "InvalidLocalPublicKey";
case HushChatCompatibilityFixtureError::InvalidPeerPublicKey:
return "InvalidPeerPublicKey";
case HushChatCompatibilityFixtureError::InvalidHeaderMemo:
return "InvalidHeaderMemo";
case HushChatCompatibilityFixtureError::InvalidMemoPair:
return "InvalidMemoPair";
case HushChatCompatibilityFixtureError::NonMemoHeader:
return "NonMemoHeader";
case HushChatCompatibilityFixtureError::HeaderPublicKeyMismatch:
return "HeaderPublicKeyMismatch";
case HushChatCompatibilityFixtureError::DecryptInputFailed:
return "DecryptInputFailed";
case HushChatCompatibilityFixtureError::NotFixtureReady:
return "NotFixtureReady";
case HushChatCompatibilityFixtureError::ExpectedStoredChatKeyLengthMismatch:
return "ExpectedStoredChatKeyLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedSeedLengthMismatch:
return "ExpectedSeedLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedLocalPublicKeyLengthMismatch:
return "ExpectedLocalPublicKeyLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedPeerPublicKeyLengthMismatch:
return "ExpectedPeerPublicKeyLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedStreamHeaderLengthMismatch:
return "ExpectedStreamHeaderLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedCiphertextLengthMismatch:
return "ExpectedCiphertextLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedPlaintextLengthMismatch:
return "ExpectedPlaintextLengthMismatch";
case HushChatCompatibilityFixtureError::ExpectedRoleMismatch:
return "ExpectedRoleMismatch";
case HushChatCompatibilityFixtureError::InvalidPlaintextHash:
return "InvalidPlaintextHash";
}
return "Unknown";
}
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind)
{
switch (kind) {
case HushChatCompatibilityFixtureKind::IncomingMemo:
return "incoming_memo";
case HushChatCompatibilityFixtureKind::OutgoingMemo:
return "outgoing_memo";
case HushChatCompatibilityFixtureKind::SeedPublicKeyProjection:
return "seed_public_key_projection";
case HushChatCompatibilityFixtureKind::CorruptedAuthFailure:
return "corrupted_auth_failure";
case HushChatCompatibilityFixtureKind::ContactExclusion:
return "cont_exclusion";
}
return "unknown";
}
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status)
{
switch (status) {
case HushChatCompatibilityFixtureFileStatus::Pending:
return "pending";
case HushChatCompatibilityFixtureFileStatus::Ready:
return "ready";
}
return "unknown";
}
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error)
{
switch (error) {
case HushChatCompatibilityFixtureFileError::None:
return "None";
case HushChatCompatibilityFixtureFileError::FeatureDisabled:
return "FeatureDisabled";
case HushChatCompatibilityFixtureFileError::InvalidJson:
return "InvalidJson";
case HushChatCompatibilityFixtureFileError::JsonNotObject:
return "JsonNotObject";
case HushChatCompatibilityFixtureFileError::InvalidSchema:
return "InvalidSchema";
case HushChatCompatibilityFixtureFileError::MissingKind:
return "MissingKind";
case HushChatCompatibilityFixtureFileError::UnknownKind:
return "UnknownKind";
case HushChatCompatibilityFixtureFileError::MissingStatus:
return "MissingStatus";
case HushChatCompatibilityFixtureFileError::UnknownStatus:
return "UnknownStatus";
case HushChatCompatibilityFixtureFileError::MissingFixtureId:
return "MissingFixtureId";
case HushChatCompatibilityFixtureFileError::MissingPendingReason:
return "MissingPendingReason";
case HushChatCompatibilityFixtureFileError::MissingFixtureObject:
return "MissingFixtureObject";
case HushChatCompatibilityFixtureFileError::InvalidFixtureField:
return "InvalidFixtureField";
case HushChatCompatibilityFixtureFileError::FixtureVerificationFailed:
return "FixtureVerificationFailed";
case HushChatCompatibilityFixtureFileError::ContactFixtureNotExcluded:
return "ContactFixtureNotExcluded";
case HushChatCompatibilityFixtureFileError::FileReadFailed:
return "FileReadFailed";
}
return "Unknown";
}
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error)
{
switch (error) {
case HushChatSeedPublicKeyProjectionError::None:
return "None";
case HushChatSeedPublicKeyProjectionError::FeatureDisabled:
return "FeatureDisabled";
case HushChatSeedPublicKeyProjectionError::MissingFixtureId:
return "MissingFixtureId";
case HushChatSeedPublicKeyProjectionError::InvalidStoredChatKey:
return "InvalidStoredChatKey";
case HushChatSeedPublicKeyProjectionError::InvalidLocalPublicKey:
return "InvalidLocalPublicKey";
case HushChatSeedPublicKeyProjectionError::ExpectedStoredChatKeyLengthMismatch:
return "ExpectedStoredChatKeyLengthMismatch";
case HushChatSeedPublicKeyProjectionError::ExpectedSeedLengthMismatch:
return "ExpectedSeedLengthMismatch";
case HushChatSeedPublicKeyProjectionError::ExpectedLocalPublicKeyLengthMismatch:
return "ExpectedLocalPublicKeyLengthMismatch";
case HushChatSeedPublicKeyProjectionError::SodiumInitializationFailed:
return "SodiumInitializationFailed";
case HushChatSeedPublicKeyProjectionError::KeypairProjectionFailed:
return "KeypairProjectionFailed";
case HushChatSeedPublicKeyProjectionError::ProjectedPublicKeyMismatch:
return "ProjectedPublicKeyMismatch";
}
return "Unknown";
}
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error)
{
switch (error) {
case HushChatCorruptedAuthFailureReadinessError::None:
return "None";
case HushChatCorruptedAuthFailureReadinessError::FeatureDisabled:
return "FeatureDisabled";
case HushChatCorruptedAuthFailureReadinessError::FixturePending:
return "FixturePending";
case HushChatCorruptedAuthFailureReadinessError::WrongFixtureKind:
return "WrongFixtureKind";
case HushChatCorruptedAuthFailureReadinessError::FixtureNotVerified:
return "FixtureNotVerified";
case HushChatCorruptedAuthFailureReadinessError::SeedProjectionNotVerified:
return "SeedProjectionNotVerified";
}
return "Unknown";
}
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error)
{
switch (error) {
case HushChatCompatibilityFixtureImportError::None:
return "None";
case HushChatCompatibilityFixtureImportError::FeatureDisabled:
return "FeatureDisabled";
case HushChatCompatibilityFixtureImportError::MissingRequiredKind:
return "MissingRequiredKind";
case HushChatCompatibilityFixtureImportError::DuplicateKind:
return "DuplicateKind";
case HushChatCompatibilityFixtureImportError::FixtureLoadFailed:
return "FixtureLoadFailed";
case HushChatCompatibilityFixtureImportError::FixtureKindMismatch:
return "FixtureKindMismatch";
case HushChatCompatibilityFixtureImportError::FixturePending:
return "FixturePending";
case HushChatCompatibilityFixtureImportError::FixtureInvalid:
return "FixtureInvalid";
case HushChatCompatibilityFixtureImportError::FixtureNotVerified:
return "FixtureNotVerified";
case HushChatCompatibilityFixtureImportError::SeedProjectionFailed:
return "SeedProjectionFailed";
case HushChatCompatibilityFixtureImportError::AuthFailureScaffoldFailed:
return "AuthFailureScaffoldFailed";
case HushChatCompatibilityFixtureImportError::ContactFixtureNotExcluded:
return "ContactFixtureNotExcluded";
}
return "Unknown";
}
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error)
{
switch (error) {
case HushChatCaptureManifestError::None:
return "None";
case HushChatCaptureManifestError::FeatureDisabled:
return "FeatureDisabled";
case HushChatCaptureManifestError::FileReadFailed:
return "FileReadFailed";
case HushChatCaptureManifestError::InvalidJson:
return "InvalidJson";
case HushChatCaptureManifestError::JsonNotObject:
return "JsonNotObject";
case HushChatCaptureManifestError::InvalidSchema:
return "InvalidSchema";
case HushChatCaptureManifestError::MissingManifestId:
return "MissingManifestId";
case HushChatCaptureManifestError::MissingStatus:
return "MissingStatus";
case HushChatCaptureManifestError::UnknownStatus:
return "UnknownStatus";
case HushChatCaptureManifestError::MissingFixtureDirectory:
return "MissingFixtureDirectory";
case HushChatCaptureManifestError::MissingDryRunCommand:
return "MissingDryRunCommand";
case HushChatCaptureManifestError::InvalidDryRunCommand:
return "InvalidDryRunCommand";
case HushChatCaptureManifestError::MissingProvenance:
return "MissingProvenance";
case HushChatCaptureManifestError::MissingSourceClient:
return "MissingSourceClient";
case HushChatCaptureManifestError::InvalidSourceClient:
return "InvalidSourceClient";
case HushChatCaptureManifestError::MissingSourceClientVersion:
return "MissingSourceClientVersion";
case HushChatCaptureManifestError::MissingCaptureDate:
return "MissingCaptureDate";
case HushChatCaptureManifestError::MissingNetwork:
return "MissingNetwork";
case HushChatCaptureManifestError::MissingCaptureMethod:
return "MissingCaptureMethod";
case HushChatCaptureManifestError::MissingHandling:
return "MissingHandling";
case HushChatCaptureManifestError::MissingHandlingFlag:
return "MissingHandlingFlag";
case HushChatCaptureManifestError::HandlingFlagNotTrue:
return "HandlingFlagNotTrue";
case HushChatCaptureManifestError::MissingCategories:
return "MissingCategories";
case HushChatCaptureManifestError::InvalidCategoryEntry:
return "InvalidCategoryEntry";
case HushChatCaptureManifestError::UnknownCategory:
return "UnknownCategory";
case HushChatCaptureManifestError::DuplicateCategory:
return "DuplicateCategory";
case HushChatCaptureManifestError::MissingRequiredCategory:
return "MissingRequiredCategory";
case HushChatCaptureManifestError::ProhibitedFieldPresent:
return "ProhibitedFieldPresent";
}
return "Unknown";
}
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction)
{
switch (direction) {
case HushChatDecryptDirection::Incoming:
return HushChatSessionKeySelection::ClientRx;
case HushChatDecryptDirection::Outgoing:
return HushChatSessionKeySelection::ServerTx;
}
return HushChatSessionKeySelection::ClientRx;
}
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<HushChatMemoOutput>& outputs)
{
struct OrderedOutput {
std::size_t input_index = 0;
HushChatMemoOutput output;
};
std::vector<OrderedOutput> 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<HushChatMemoOutput> pending_header_output;
std::optional<HushChatHeader> 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();
result.metadata.push_back(std::move(metadata));
}
return result;
}
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
const HushChatDecryptPreflightInput& input,
bool featureEnabled)
{
auto failPreflight = [&](HushChatDecryptPreflightError error) {
HushChatDecryptPreflightResult result;
result.feature_enabled = featureEnabled;
result.error = error;
result.error_name = hushChatDecryptPreflightErrorName(error);
return result;
};
if (!featureEnabled) return failPreflight(HushChatDecryptPreflightError::FeatureDisabled);
const auto& header = input.header;
if (header.type != HushChatHeaderType::Message) {
return failPreflight(HushChatDecryptPreflightError::NonMessageHeader);
}
if (header.header_number < 1) return failPreflight(HushChatDecryptPreflightError::InvalidHeaderNumber);
if (header.version != kHushChatSupportedVersion) {
return failPreflight(HushChatDecryptPreflightError::UnsupportedVersion);
}
if (header.reply_zaddr.empty()) return failPreflight(HushChatDecryptPreflightError::MissingReplyAddress);
if (header.conversation_id.empty()) return failPreflight(HushChatDecryptPreflightError::MissingConversationId);
if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength ||
!isHexString(header.secretstream_header_hex)) {
return failPreflight(HushChatDecryptPreflightError::InvalidSecretstreamHeader);
}
if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) {
return failPreflight(HushChatDecryptPreflightError::InvalidPublicKey);
}
if (input.ciphertext_hex.empty()) return failPreflight(HushChatDecryptPreflightError::EmptyCiphertext);
if (input.ciphertext_hex.size() > kHushChatMemoByteLimit) {
return failPreflight(HushChatDecryptPreflightError::OversizedCiphertext);
}
if (input.ciphertext_hex.size() % 2 != 0) return failPreflight(HushChatDecryptPreflightError::OddLengthCiphertext);
if (!isHexString(input.ciphertext_hex)) return failPreflight(HushChatDecryptPreflightError::InvalidCiphertextHex);
const std::size_t ciphertextSize = input.ciphertext_hex.size() / 2;
if (ciphertextSize <= kHushChatSecretstreamABytes) {
return failPreflight(HushChatDecryptPreflightError::TruncatedCiphertext);
}
HushChatDecryptPreflightResult result;
result.ok = true;
result.feature_enabled = true;
result.error = HushChatDecryptPreflightError::None;
result.error_name = hushChatDecryptPreflightErrorName(result.error);
result.ciphertext_size = ciphertextSize;
return result;
}
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
std::size_t expectedByteLength)
{
auto failDecode = [](HushChatHexDecodeError error) {
HushChatHexDecodeResult result;
result.error = error;
result.error_name = hushChatHexDecodeErrorName(error);
return result;
};
if (hex.empty()) {
if (expectedByteLength == 0) {
HushChatHexDecodeResult result;
result.ok = true;
return result;
}
return failDecode(HushChatHexDecodeError::Empty);
}
if (hex.size() % 2 != 0) return failDecode(HushChatHexDecodeError::OddLength);
if (!isHexString(hex)) return failDecode(HushChatHexDecodeError::InvalidHex);
if (hex.size() / 2 != expectedByteLength) {
return failDecode(HushChatHexDecodeError::UnexpectedByteLength);
}
HushChatHexDecodeResult result;
result.ok = true;
result.bytes.reserve(expectedByteLength);
for (std::size_t index = 0; index < hex.size(); index += 2) {
auto high = hexNibble(static_cast<unsigned char>(hex[index]));
auto low = hexNibble(static_cast<unsigned char>(hex[index + 1]));
if (!high || !low) return failDecode(HushChatHexDecodeError::InvalidHex);
result.bytes.push_back(static_cast<unsigned char>((*high << 4) | *low));
}
return result;
}
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
const HushChatDecryptInputMaterial& material,
bool featureEnabled)
{
auto failPreparation = [&](HushChatDecryptInputError error,
HushChatHexDecodeError hexError = HushChatHexDecodeError::None,
HushChatDecryptPreflightError preflightError = HushChatDecryptPreflightError::None) {
HushChatDecryptInputPreparationResult result;
result.feature_enabled = featureEnabled;
result.error = error;
result.error_name = hushChatDecryptInputErrorName(error);
result.hex_error = hexError;
result.preflight_error = preflightError;
return result;
};
if (!featureEnabled) return failPreparation(HushChatDecryptInputError::FeatureDisabled);
auto storedChatKey = decodeHushChatHexBytes(material.stored_chat_key_hex,
kHushChatStoredChatKeyByteLength);
if (!storedChatKey.ok) {
return failPreparation(HushChatDecryptInputError::InvalidStoredChatKey, storedChatKey.error);
}
auto preflight = validateHushChatMemoDecryptPreflight(
HushChatDecryptPreflightInput{material.header, material.ciphertext_hex},
true);
if (!preflight.ok) {
return failPreparation(HushChatDecryptInputError::DecryptPreflightFailed,
HushChatHexDecodeError::None,
preflight.error);
}
const std::string& peerPublicKeyHex = material.peer_public_key_hex.empty()
? material.header.public_key_hex
: material.peer_public_key_hex;
auto peerPublicKey = decodeHushChatHexBytes(peerPublicKeyHex,
kHushChatPublicKeyByteLength);
if (!peerPublicKey.ok) {
return failPreparation(HushChatDecryptInputError::InvalidPeerPublicKey, peerPublicKey.error);
}
auto streamHeader = decodeHushChatHexBytes(material.header.secretstream_header_hex,
kHushChatSecretstreamHeaderByteLength);
if (!streamHeader.ok) {
return failPreparation(HushChatDecryptInputError::InvalidStreamHeader, streamHeader.error);
}
auto ciphertext = decodeHushChatHexBytes(material.ciphertext_hex, preflight.ciphertext_size);
if (!ciphertext.ok) {
return failPreparation(HushChatDecryptInputError::InvalidCiphertext, ciphertext.error);
}
HushChatDecryptInputPreparationResult result;
result.ok = true;
result.feature_enabled = true;
result.error = HushChatDecryptInputError::None;
result.error_name = hushChatDecryptInputErrorName(result.error);
result.prepared.stored_chat_key_bytes = std::move(storedChatKey.bytes);
result.prepared.seed_bytes.reserve(kHushChatSeedByteLength);
for (std::size_t index = 0; index < kHushChatSeedByteLength; ++index) {
result.prepared.seed_bytes.push_back(static_cast<unsigned char>(material.stored_chat_key_hex[index]));
}
result.prepared.peer_public_key_bytes = std::move(peerPublicKey.bytes);
result.prepared.stream_header_bytes = std::move(streamHeader.bytes);
result.prepared.ciphertext_bytes = std::move(ciphertext.bytes);
result.prepared.direction = material.direction;
result.prepared.session_key_selection = hushChatSessionKeySelectionForDirection(material.direction);
result.prepared.plaintext_capacity = result.prepared.ciphertext_bytes.size() - kHushChatSecretstreamABytes;
return result;
}
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
const HushChatPreparedDecryptInput& prepared)
{
HushChatDecryptFixtureReadinessResult result;
result.stored_chat_key_size = prepared.stored_chat_key_bytes.size();
result.seed_size = prepared.seed_bytes.size();
result.peer_public_key_size = prepared.peer_public_key_bytes.size();
result.stream_header_size = prepared.stream_header_bytes.size();
result.ciphertext_size = prepared.ciphertext_bytes.size();
result.plaintext_capacity = prepared.plaintext_capacity;
result.session_key_selection = prepared.session_key_selection;
result.ready = result.stored_chat_key_size == kHushChatStoredChatKeyByteLength &&
result.seed_size == kHushChatSeedByteLength &&
result.peer_public_key_size == kHushChatPublicKeyByteLength &&
result.stream_header_size == kHushChatSecretstreamHeaderByteLength &&
result.ciphertext_size > kHushChatSecretstreamABytes &&
result.plaintext_capacity == result.ciphertext_size - kHushChatSecretstreamABytes;
return result;
}
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled)
{
auto failFixture = [&](HushChatCompatibilityFixtureError error,
HushChatHexDecodeError hexError = HushChatHexDecodeError::None) {
HushChatCompatibilityFixtureVerificationResult result;
result.feature_enabled = featureEnabled;
result.error = error;
result.error_name = hushChatCompatibilityFixtureErrorName(error);
result.hex_error = hexError;
return result;
};
if (!featureEnabled) return failFixture(HushChatCompatibilityFixtureError::FeatureDisabled);
if (fixture.fixture_id.empty()) return failFixture(HushChatCompatibilityFixtureError::MissingFixtureId);
auto localPublicKey = decodeHushChatHexBytes(fixture.local_public_key_hex,
kHushChatPublicKeyByteLength);
if (!localPublicKey.ok) {
return failFixture(HushChatCompatibilityFixtureError::InvalidLocalPublicKey, localPublicKey.error);
}
auto peerPublicKey = decodeHushChatHexBytes(fixture.peer_public_key_hex,
kHushChatPublicKeyByteLength);
if (!peerPublicKey.ok) {
return failFixture(HushChatCompatibilityFixtureError::InvalidPeerPublicKey, peerPublicKey.error);
}
auto parsedHeader = parseHushChatHeaderMemo(fixture.header_memo);
if (!parsedHeader.ok) return failFixture(HushChatCompatibilityFixtureError::InvalidHeaderMemo);
auto grouped = groupHushChatMemoOutputs({
HushChatMemoOutput{0, fixture.header_memo},
HushChatMemoOutput{1, fixture.ciphertext_memo}
});
if (grouped.pairs.size() != 1 || !grouped.issues.empty()) {
return failFixture(HushChatCompatibilityFixtureError::InvalidMemoPair);
}
const auto& header = grouped.pairs[0].header;
if (header.type != HushChatHeaderType::Message) {
return failFixture(HushChatCompatibilityFixtureError::NonMemoHeader);
}
const std::string& expectedHeaderPublicKey = fixture.direction == HushChatDecryptDirection::Incoming
? fixture.peer_public_key_hex
: fixture.local_public_key_hex;
if (header.public_key_hex != expectedHeaderPublicKey) {
return failFixture(HushChatCompatibilityFixtureError::HeaderPublicKeyMismatch);
}
auto preparation = prepareHushChatDecryptInput(HushChatDecryptInputMaterial{
fixture.stored_chat_key_hex,
header,
fixture.ciphertext_memo,
fixture.direction,
fixture.peer_public_key_hex
}, true);
if (!preparation.ok) {
HushChatCompatibilityFixtureVerificationResult result = failFixture(
HushChatCompatibilityFixtureError::DecryptInputFailed,
preparation.hex_error);
result.decrypt_input_error = preparation.error;
result.preflight_error = preparation.preflight_error;
result.preparation = std::move(preparation);
return result;
}
auto readiness = inspectHushChatDecryptFixtureReadiness(preparation.prepared);
if (!readiness.ready) {
HushChatCompatibilityFixtureVerificationResult result = failFixture(
HushChatCompatibilityFixtureError::NotFixtureReady);
result.preparation = std::move(preparation);
result.readiness = readiness;
return result;
}
auto failExpectation = [&](HushChatCompatibilityFixtureError error,
HushChatDecryptInputPreparationResult&& prepared,
const HushChatDecryptFixtureReadinessResult& fixtureReadiness) {
HushChatCompatibilityFixtureVerificationResult result = failFixture(error);
result.header = header;
result.preparation = std::move(prepared);
result.readiness = fixtureReadiness;
result.local_public_key_size = localPublicKey.bytes.size();
result.peer_public_key_size = peerPublicKey.bytes.size();
return result;
};
if (readiness.stored_chat_key_size != fixture.expected_stored_chat_key_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedStoredChatKeyLengthMismatch,
std::move(preparation),
readiness);
}
if (readiness.seed_size != fixture.expected_seed_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedSeedLengthMismatch,
std::move(preparation),
readiness);
}
if (localPublicKey.bytes.size() != fixture.expected_local_public_key_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedLocalPublicKeyLengthMismatch,
std::move(preparation),
readiness);
}
if (readiness.peer_public_key_size != fixture.expected_peer_public_key_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedPeerPublicKeyLengthMismatch,
std::move(preparation),
readiness);
}
if (readiness.stream_header_size != fixture.expected_stream_header_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedStreamHeaderLengthMismatch,
std::move(preparation),
readiness);
}
if (readiness.ciphertext_size != fixture.expected_ciphertext_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedCiphertextLengthMismatch,
std::move(preparation),
readiness);
}
if (readiness.plaintext_capacity != fixture.expected_plaintext_size) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedPlaintextLengthMismatch,
std::move(preparation),
readiness);
}
if (readiness.session_key_selection != fixture.expected_session_key_selection) {
return failExpectation(HushChatCompatibilityFixtureError::ExpectedRoleMismatch,
std::move(preparation),
readiness);
}
std::size_t plaintextHashSize = 0;
if (!fixture.expected_plaintext_hash_hex.empty()) {
if (fixture.expected_plaintext_hash_hex.size() % 2 != 0 ||
!isHexString(fixture.expected_plaintext_hash_hex)) {
return failExpectation(HushChatCompatibilityFixtureError::InvalidPlaintextHash,
std::move(preparation),
readiness);
}
plaintextHashSize = fixture.expected_plaintext_hash_hex.size() / 2;
}
HushChatCompatibilityFixtureVerificationResult result;
result.ok = true;
result.feature_enabled = true;
result.error = HushChatCompatibilityFixtureError::None;
result.error_name = hushChatCompatibilityFixtureErrorName(result.error);
result.header = header;
result.preparation = std::move(preparation);
result.readiness = readiness;
result.local_public_key_size = localPublicKey.bytes.size();
result.peer_public_key_size = peerPublicKey.bytes.size();
result.plaintext_hash_size = plaintextHashSize;
return result;
}
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
const std::string& jsonText,
bool featureEnabled)
{
auto failFile = [&](HushChatCompatibilityFixtureFileError error) {
HushChatCompatibilityFixtureFileParseResult result;
result.feature_enabled = featureEnabled;
result.error = error;
result.error_name = hushChatCompatibilityFixtureFileErrorName(error);
return result;
};
if (!featureEnabled) return failFile(HushChatCompatibilityFixtureFileError::FeatureDisabled);
nlohmann::json object;
try {
object = nlohmann::json::parse(jsonText);
} catch (const nlohmann::json::parse_error&) {
return failFile(HushChatCompatibilityFixtureFileError::InvalidJson);
}
if (!object.is_object()) return failFile(HushChatCompatibilityFixtureFileError::JsonNotObject);
auto schema = readJsonString(object, "schema");
if (!schema || *schema != kHushChatCompatibilityFixtureSchema) {
return failFile(HushChatCompatibilityFixtureFileError::InvalidSchema);
}
auto kindText = readJsonString(object, "kind");
if (!kindText) return failFile(HushChatCompatibilityFixtureFileError::MissingKind);
auto kind = parseFixtureKind(*kindText);
if (!kind) return failFile(HushChatCompatibilityFixtureFileError::UnknownKind);
auto statusText = readJsonString(object, "status");
if (!statusText) return failFile(HushChatCompatibilityFixtureFileError::MissingStatus);
auto status = parseFixtureFileStatus(*statusText);
if (!status) return failFile(HushChatCompatibilityFixtureFileError::UnknownStatus);
HushChatCompatibilityFixtureFile parsed;
parsed.schema = *schema;
parsed.kind = *kind;
parsed.status = *status;
if (auto fixtureId = readJsonString(object, "id")) parsed.fixture_id = *fixtureId;
if (parsed.status == HushChatCompatibilityFixtureFileStatus::Pending) {
if (parsed.fixture_id.empty()) return failFile(HushChatCompatibilityFixtureFileError::MissingFixtureId);
auto pendingReason = readJsonString(object, "pending_reason");
if (!pendingReason || pendingReason->empty()) {
return failFile(HushChatCompatibilityFixtureFileError::MissingPendingReason);
}
parsed.pending_reason = *pendingReason;
HushChatCompatibilityFixtureFileParseResult result;
result.ok = true;
result.feature_enabled = true;
result.pending = true;
result.error = HushChatCompatibilityFixtureFileError::None;
result.error_name = hushChatCompatibilityFixtureFileErrorName(result.error);
result.file = std::move(parsed);
return result;
}
if (!object.contains("fixture") || !object["fixture"].is_object()) {
return failFile(HushChatCompatibilityFixtureFileError::MissingFixtureObject);
}
const auto& fixtureObject = object["fixture"];
const nlohmann::json expected = fixtureObject.contains("expected") ? fixtureObject["expected"] : nlohmann::json();
if (!expected.is_object()) return failFile(HushChatCompatibilityFixtureFileError::InvalidFixtureField);
auto readRequiredFixtureString = [&](const nlohmann::json& source, const char* key) -> std::optional<std::string> {
auto value = readJsonString(source, key);
if (!value || value->empty()) return std::nullopt;
return value;
};
HushChatCompatibilityFixture fixture;
auto id = readRequiredFixtureString(fixtureObject, "id");
auto storedChatKey = readRequiredFixtureString(fixtureObject, "stored_chat_key_hex");
auto localPublicKey = readRequiredFixtureString(fixtureObject, "local_public_key_hex");
auto peerPublicKey = readRequiredFixtureString(fixtureObject, "peer_public_key_hex");
auto headerMemo = readRequiredFixtureString(fixtureObject, "header_memo");
auto ciphertextMemo = readRequiredFixtureString(fixtureObject, "ciphertext_memo");
auto directionText = readRequiredFixtureString(fixtureObject, "direction");
auto roleText = readRequiredFixtureString(expected, "session_key_selection");
auto storedKeySize = readJsonSize(expected, "stored_chat_key_bytes");
auto seedSize = readJsonSize(expected, "seed_bytes");
auto localKeySize = readJsonSize(expected, "local_public_key_bytes");
auto peerKeySize = readJsonSize(expected, "peer_public_key_bytes");
auto streamHeaderSize = readJsonSize(expected, "stream_header_bytes");
auto ciphertextSize = readJsonSize(expected, "ciphertext_bytes");
auto plaintextSize = readJsonSize(expected, "plaintext_bytes");
if (!id || !storedChatKey || !localPublicKey || !peerPublicKey || !headerMemo || !ciphertextMemo ||
!directionText || !roleText || !storedKeySize || !seedSize || !localKeySize || !peerKeySize ||
!streamHeaderSize || !ciphertextSize || !plaintextSize) {
return failFile(HushChatCompatibilityFixtureFileError::InvalidFixtureField);
}
auto direction = parseFixtureDirection(*directionText);
auto role = parseFixtureSessionKeySelection(*roleText);
if (!direction || !role) return failFile(HushChatCompatibilityFixtureFileError::InvalidFixtureField);
fixture.fixture_id = *id;
fixture.stored_chat_key_hex = *storedChatKey;
fixture.local_public_key_hex = *localPublicKey;
fixture.peer_public_key_hex = *peerPublicKey;
fixture.header_memo = *headerMemo;
fixture.ciphertext_memo = *ciphertextMemo;
fixture.direction = *direction;
fixture.expected_session_key_selection = *role;
fixture.expected_stored_chat_key_size = *storedKeySize;
fixture.expected_seed_size = *seedSize;
fixture.expected_local_public_key_size = *localKeySize;
fixture.expected_peer_public_key_size = *peerKeySize;
fixture.expected_stream_header_size = *streamHeaderSize;
fixture.expected_ciphertext_size = *ciphertextSize;
fixture.expected_plaintext_size = *plaintextSize;
if (auto plaintextHash = readJsonString(expected, "plaintext_hash_hex")) {
fixture.expected_plaintext_hash_hex = *plaintextHash;
}
parsed.fixture_id = fixture.fixture_id;
parsed.fixture = fixture;
auto verification = verifyHushChatCompatibilityFixture(fixture, true);
if (parsed.kind == HushChatCompatibilityFixtureKind::ContactExclusion) {
if (verification.error != HushChatCompatibilityFixtureError::NonMemoHeader) {
HushChatCompatibilityFixtureFileParseResult result = failFile(
HushChatCompatibilityFixtureFileError::ContactFixtureNotExcluded);
result.file = std::move(parsed);
result.verification = std::move(verification);
return result;
}
HushChatCompatibilityFixtureFileParseResult result;
result.ok = true;
result.feature_enabled = true;
result.excluded_from_decrypt = true;
result.error = HushChatCompatibilityFixtureFileError::None;
result.error_name = hushChatCompatibilityFixtureFileErrorName(result.error);
result.file = std::move(parsed);
result.verification = std::move(verification);
return result;
}
if (!verification.ok) {
HushChatCompatibilityFixtureFileParseResult result = failFile(
HushChatCompatibilityFixtureFileError::FixtureVerificationFailed);
result.file = std::move(parsed);
result.verification = std::move(verification);
return result;
}
HushChatCompatibilityFixtureFileParseResult result;
result.ok = true;
result.feature_enabled = true;
result.verified = true;
result.error = HushChatCompatibilityFixtureFileError::None;
result.error_name = hushChatCompatibilityFixtureFileErrorName(result.error);
result.file = std::move(parsed);
result.verification = std::move(verification);
return result;
}
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
const std::string& path,
bool featureEnabled)
{
if (!featureEnabled) {
HushChatCompatibilityFixtureFileParseResult result;
result.error = HushChatCompatibilityFixtureFileError::FeatureDisabled;
result.error_name = hushChatCompatibilityFixtureFileErrorName(result.error);
return result;
}
std::ifstream input(path);
if (!input.good()) {
HushChatCompatibilityFixtureFileParseResult result;
result.feature_enabled = true;
result.error = HushChatCompatibilityFixtureFileError::FileReadFailed;
result.error_name = hushChatCompatibilityFixtureFileErrorName(result.error);
return result;
}
std::ostringstream buffer;
buffer << input.rdbuf();
return parseHushChatCompatibilityFixtureFile(buffer.str(), true);
}
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled)
{
auto failProjection = [&](HushChatSeedPublicKeyProjectionError error,
HushChatHexDecodeError hexError = HushChatHexDecodeError::None) {
HushChatSeedPublicKeyProjectionResult result;
result.feature_enabled = featureEnabled;
result.error = error;
result.error_name = hushChatSeedPublicKeyProjectionErrorName(error);
result.hex_error = hexError;
return result;
};
if (!featureEnabled) return failProjection(HushChatSeedPublicKeyProjectionError::FeatureDisabled);
if (fixture.fixture_id.empty()) return failProjection(HushChatSeedPublicKeyProjectionError::MissingFixtureId);
auto storedChatKey = decodeHushChatHexBytes(fixture.stored_chat_key_hex,
kHushChatStoredChatKeyByteLength);
if (!storedChatKey.ok) {
return failProjection(HushChatSeedPublicKeyProjectionError::InvalidStoredChatKey, storedChatKey.error);
}
auto localPublicKey = decodeHushChatHexBytes(fixture.local_public_key_hex,
kHushChatPublicKeyByteLength);
if (!localPublicKey.ok) {
return failProjection(HushChatSeedPublicKeyProjectionError::InvalidLocalPublicKey, localPublicKey.error);
}
const std::size_t seedSize = std::min(fixture.stored_chat_key_hex.size(), kHushChatSeedByteLength);
if (storedChatKey.bytes.size() != fixture.expected_stored_chat_key_size) {
auto result = failProjection(HushChatSeedPublicKeyProjectionError::ExpectedStoredChatKeyLengthMismatch);
result.stored_chat_key_size = storedChatKey.bytes.size();
result.seed_size = seedSize;
result.local_public_key_size = localPublicKey.bytes.size();
return result;
}
if (seedSize != fixture.expected_seed_size) {
auto result = failProjection(HushChatSeedPublicKeyProjectionError::ExpectedSeedLengthMismatch);
result.stored_chat_key_size = storedChatKey.bytes.size();
result.seed_size = seedSize;
result.local_public_key_size = localPublicKey.bytes.size();
return result;
}
if (localPublicKey.bytes.size() != fixture.expected_local_public_key_size) {
auto result = failProjection(HushChatSeedPublicKeyProjectionError::ExpectedLocalPublicKeyLengthMismatch);
result.stored_chat_key_size = storedChatKey.bytes.size();
result.seed_size = seedSize;
result.local_public_key_size = localPublicKey.bytes.size();
return result;
}
if (sodium_init() < 0) {
return failProjection(HushChatSeedPublicKeyProjectionError::SodiumInitializationFailed);
}
unsigned char seed[kHushChatSeedByteLength];
for (std::size_t index = 0; index < kHushChatSeedByteLength; ++index) {
seed[index] = static_cast<unsigned char>(fixture.stored_chat_key_hex[index]);
}
unsigned char projectedPublicKey[crypto_kx_PUBLICKEYBYTES];
unsigned char projectedSecretKey[crypto_kx_SECRETKEYBYTES];
const int projectionStatus = crypto_kx_seed_keypair(projectedPublicKey, projectedSecretKey, seed);
sodium_memzero(projectedSecretKey, sizeof(projectedSecretKey));
sodium_memzero(seed, sizeof(seed));
if (projectionStatus != 0) {
return failProjection(HushChatSeedPublicKeyProjectionError::KeypairProjectionFailed);
}
HushChatSeedPublicKeyProjectionResult result;
result.feature_enabled = true;
result.stored_chat_key_size = storedChatKey.bytes.size();
result.seed_size = seedSize;
result.local_public_key_size = localPublicKey.bytes.size();
result.projected_public_key_size = sizeof(projectedPublicKey);
if (result.projected_public_key_size != localPublicKey.bytes.size() ||
std::memcmp(projectedPublicKey, localPublicKey.bytes.data(), localPublicKey.bytes.size()) != 0) {
sodium_memzero(projectedPublicKey, sizeof(projectedPublicKey));
result.error = HushChatSeedPublicKeyProjectionError::ProjectedPublicKeyMismatch;
result.error_name = hushChatSeedPublicKeyProjectionErrorName(result.error);
return result;
}
sodium_memzero(projectedPublicKey, sizeof(projectedPublicKey));
result.ok = true;
result.error = HushChatSeedPublicKeyProjectionError::None;
result.error_name = hushChatSeedPublicKeyProjectionErrorName(result.error);
return result;
}
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
const HushChatCompatibilityFixtureFileParseResult& parsed,
const HushChatSeedPublicKeyProjectionResult& seedProjection,
bool featureEnabled)
{
auto failReadiness = [&](HushChatCorruptedAuthFailureReadinessError error) {
HushChatCorruptedAuthFailureReadinessResult result;
result.feature_enabled = featureEnabled;
result.error = error;
result.error_name = hushChatCorruptedAuthFailureReadinessErrorName(error);
return result;
};
if (!featureEnabled) return failReadiness(HushChatCorruptedAuthFailureReadinessError::FeatureDisabled);
if (parsed.pending) return failReadiness(HushChatCorruptedAuthFailureReadinessError::FixturePending);
if (parsed.file.kind != HushChatCompatibilityFixtureKind::CorruptedAuthFailure) {
return failReadiness(HushChatCorruptedAuthFailureReadinessError::WrongFixtureKind);
}
if (!parsed.verified || !parsed.verification.ok) {
return failReadiness(HushChatCorruptedAuthFailureReadinessError::FixtureNotVerified);
}
if (!seedProjection.ok) {
return failReadiness(HushChatCorruptedAuthFailureReadinessError::SeedProjectionNotVerified);
}
HushChatCorruptedAuthFailureReadinessResult result;
result.ok = true;
result.feature_enabled = true;
result.structurally_ready_for_future_auth_check = true;
result.requires_future_secretstream_auth_failure = true;
result.decrypted = false;
result.authenticated = false;
result.error = HushChatCorruptedAuthFailureReadinessError::None;
result.error_name = hushChatCorruptedAuthFailureReadinessErrorName(result.error);
return result;
}
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds()
{
return {
HushChatCompatibilityFixtureKind::IncomingMemo,
HushChatCompatibilityFixtureKind::OutgoingMemo,
HushChatCompatibilityFixtureKind::SeedPublicKeyProjection,
HushChatCompatibilityFixtureKind::CorruptedAuthFailure,
HushChatCompatibilityFixtureKind::ContactExclusion
};
}
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled)
{
HushChatCompatibilityFixtureImportChecklistResult result;
result.feature_enabled = featureEnabled;
auto setFirstError = [&](HushChatCompatibilityFixtureImportError error) {
if (result.error == HushChatCompatibilityFixtureImportError::None) {
result.error = error;
result.error_name = hushChatCompatibilityFixtureImportErrorName(error);
}
};
if (!featureEnabled) {
setFirstError(HushChatCompatibilityFixtureImportError::FeatureDisabled);
return result;
}
const auto requiredKinds = hushChatRequiredCompatibilityFixtureKinds();
result.required_count = requiredKinds.size();
result.items.reserve(requiredKinds.size());
for (const auto expectedKind : requiredKinds) {
HushChatCompatibilityFixtureImportItem item;
item.expected_kind = expectedKind;
const HushChatCompatibilityFixtureImportCandidate* selected = nullptr;
std::size_t matchCount = 0;
for (const auto& candidate : candidates) {
if (candidate.expected_kind != expectedKind) continue;
++matchCount;
if (!selected) selected = &candidate;
}
if (matchCount == 0) {
item.error = HushChatCompatibilityFixtureImportError::MissingRequiredKind;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.missing_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
item.supplied = true;
++result.supplied_count;
if (matchCount > 1 || !selected) {
item.error = HushChatCompatibilityFixtureImportError::DuplicateKind;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
item.path = selected->path;
item.parsed = loadHushChatCompatibilityFixtureFile(selected->path, true);
if (!item.parsed.ok) {
if (item.parsed.error == HushChatCompatibilityFixtureFileError::FileReadFailed) {
item.error = HushChatCompatibilityFixtureImportError::FixtureLoadFailed;
} else if (item.parsed.error == HushChatCompatibilityFixtureFileError::ContactFixtureNotExcluded) {
item.error = HushChatCompatibilityFixtureImportError::ContactFixtureNotExcluded;
} else {
item.error = HushChatCompatibilityFixtureImportError::FixtureInvalid;
}
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
item.loaded_kind = item.parsed.file.kind;
if (item.loaded_kind != expectedKind) {
item.error = HushChatCompatibilityFixtureImportError::FixtureKindMismatch;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
if (item.parsed.pending) {
item.pending = true;
item.error = HushChatCompatibilityFixtureImportError::FixturePending;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.pending_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
if (expectedKind == HushChatCompatibilityFixtureKind::ContactExclusion) {
if (!item.parsed.excluded_from_decrypt) {
item.error = HushChatCompatibilityFixtureImportError::ContactFixtureNotExcluded;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
item.replacement_eligible = true;
item.error = HushChatCompatibilityFixtureImportError::None;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.excluded_count;
result.items.push_back(std::move(item));
continue;
}
if (!item.parsed.verified) {
item.error = HushChatCompatibilityFixtureImportError::FixtureNotVerified;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
item.seed_projection = verifyHushChatSeedPublicKeyProjection(item.parsed.file.fixture, true);
if (!item.seed_projection.ok) {
item.error = HushChatCompatibilityFixtureImportError::SeedProjectionFailed;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
if (expectedKind == HushChatCompatibilityFixtureKind::CorruptedAuthFailure) {
item.auth_failure_readiness = inspectHushChatCorruptedAuthFailureReadiness(
item.parsed,
item.seed_projection,
true);
if (!item.auth_failure_readiness.ok) {
item.error = HushChatCompatibilityFixtureImportError::AuthFailureScaffoldFailed;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.rejected_count;
setFirstError(item.error);
result.items.push_back(std::move(item));
continue;
}
item.future_auth_failure_required = item.auth_failure_readiness.requires_future_secretstream_auth_failure;
item.structurally_ready_for_future_auth_check =
item.auth_failure_readiness.structurally_ready_for_future_auth_check;
if (item.future_auth_failure_required) ++result.future_auth_failure_required_count;
if (item.structurally_ready_for_future_auth_check) ++result.auth_failure_structural_ready_count;
}
item.replacement_eligible = true;
item.seed_projection_verified = true;
item.error = HushChatCompatibilityFixtureImportError::None;
item.error_name = hushChatCompatibilityFixtureImportErrorName(item.error);
++result.verified_count;
++result.seed_projection_verified_count;
result.items.push_back(std::move(item));
}
result.replacement_ready = result.supplied_count == result.required_count &&
result.missing_count == 0 &&
result.pending_count == 0 &&
result.rejected_count == 0 &&
result.verified_count == result.required_count - 1 &&
result.seed_projection_verified_count == result.required_count - 1 &&
result.future_auth_failure_required_count == 1 &&
result.auth_failure_structural_ready_count == 1 &&
result.excluded_count == 1;
result.ok = result.replacement_ready;
if (result.ok) {
result.error = HushChatCompatibilityFixtureImportError::None;
result.error_name = hushChatCompatibilityFixtureImportErrorName(result.error);
}
return result;
}
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled)
{
const auto checklist = inspectHushChatCompatibilityFixtureImportChecklist(candidates, featureEnabled);
HushChatCompatibilityFixtureReplacementDryRunResult result;
result.ok = checklist.replacement_ready;
result.feature_enabled = checklist.feature_enabled;
result.would_replace = checklist.replacement_ready;
result.replacement_refused = !checklist.replacement_ready;
result.error = checklist.error;
result.error_name = checklist.error_name;
result.required_count = checklist.required_count;
result.supplied_count = checklist.supplied_count;
result.missing_count = checklist.missing_count;
result.pending_count = checklist.pending_count;
result.verified_count = checklist.verified_count;
result.seed_projection_verified_count = checklist.seed_projection_verified_count;
result.future_auth_failure_required_count = checklist.future_auth_failure_required_count;
result.auth_failure_structural_ready_count = checklist.auth_failure_structural_ready_count;
result.excluded_count = checklist.excluded_count;
result.rejected_count = checklist.rejected_count;
result.report_items.reserve(checklist.items.size());
for (const auto& item : checklist.items) {
HushChatCompatibilityFixtureReplacementReportItem reportItem;
reportItem.expected_kind = item.expected_kind;
reportItem.loaded_kind = item.loaded_kind;
reportItem.path = item.path;
reportItem.supplied = item.supplied;
reportItem.pending = item.pending;
reportItem.replacement_eligible = item.replacement_eligible;
reportItem.refused = !item.replacement_eligible;
reportItem.seed_projection_verified = item.seed_projection_verified;
reportItem.future_auth_failure_required = item.future_auth_failure_required;
reportItem.structurally_ready_for_future_auth_check = item.structurally_ready_for_future_auth_check;
reportItem.cont_excluded = item.parsed.excluded_from_decrypt;
reportItem.decrypted = item.auth_failure_readiness.decrypted;
reportItem.authenticated = item.auth_failure_readiness.authenticated;
reportItem.error = item.error;
reportItem.error_name = item.error_name;
result.report_items.push_back(std::move(reportItem));
}
return result;
}
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
const std::string& jsonText,
bool featureEnabled)
{
HushChatCaptureManifestValidationResult result;
result.feature_enabled = featureEnabled;
const auto requiredKinds = hushChatRequiredCompatibilityFixtureKinds();
result.required_count = requiredKinds.size();
result.categories.reserve(requiredKinds.size());
for (const auto kind : requiredKinds) {
HushChatCaptureManifestCategoryReport report;
report.kind = kind;
result.categories.push_back(std::move(report));
}
auto failManifest = [&](HushChatCaptureManifestError error) {
result.error = error;
result.error_name = hushChatCaptureManifestErrorName(error);
return result;
};
if (!featureEnabled) return failManifest(HushChatCaptureManifestError::FeatureDisabled);
nlohmann::json object;
try {
object = nlohmann::json::parse(jsonText);
} catch (const nlohmann::json::parse_error&) {
return failManifest(HushChatCaptureManifestError::InvalidJson);
}
if (!object.is_object()) return failManifest(HushChatCaptureManifestError::JsonNotObject);
result.prohibited_field_count = countProhibitedCaptureManifestFields(object);
if (result.prohibited_field_count > 0) {
return failManifest(HushChatCaptureManifestError::ProhibitedFieldPresent);
}
auto schema = readJsonString(object, "schema");
if (!schema || *schema != kHushChatCaptureManifestSchema) {
return failManifest(HushChatCaptureManifestError::InvalidSchema);
}
auto id = readJsonString(object, "id");
if (!id || id->empty()) return failManifest(HushChatCaptureManifestError::MissingManifestId);
auto status = readJsonString(object, "status");
if (!status) return failManifest(HushChatCaptureManifestError::MissingStatus);
if (*status != "staged") return failManifest(HushChatCaptureManifestError::UnknownStatus);
auto fixtureDirectory = readJsonString(object, "fixture_directory");
if (!fixtureDirectory || fixtureDirectory->empty()) {
return failManifest(HushChatCaptureManifestError::MissingFixtureDirectory);
}
result.fixture_directory = *fixtureDirectory;
auto dryRunCommand = readJsonString(object, "dry_run_command");
if (!dryRunCommand || dryRunCommand->empty()) {
return failManifest(HushChatCaptureManifestError::MissingDryRunCommand);
}
result.has_dry_run_command = true;
if (dryRunCommand->find("HushChatFixtureCheck") == std::string::npos ||
dryRunCommand->find("--replacement-dry-run") == std::string::npos) {
return failManifest(HushChatCaptureManifestError::InvalidDryRunCommand);
}
auto provenance = object.find("provenance");
if (provenance == object.end() || !provenance->is_object()) {
return failManifest(HushChatCaptureManifestError::MissingProvenance);
}
auto sourceClient = readJsonString(*provenance, "source_client");
if (!sourceClient || sourceClient->empty()) {
return failManifest(HushChatCaptureManifestError::MissingSourceClient);
}
if (*sourceClient != "SilentDragonXLite") {
return failManifest(HushChatCaptureManifestError::InvalidSourceClient);
}
auto sourceClientVersion = readJsonString(*provenance, "source_client_version");
if (!sourceClientVersion || sourceClientVersion->empty()) {
return failManifest(HushChatCaptureManifestError::MissingSourceClientVersion);
}
auto captureDate = readJsonString(*provenance, "capture_date");
if (!captureDate || captureDate->empty()) return failManifest(HushChatCaptureManifestError::MissingCaptureDate);
auto network = readJsonString(*provenance, "network");
if (!network || network->empty()) return failManifest(HushChatCaptureManifestError::MissingNetwork);
auto captureMethod = readJsonString(*provenance, "capture_method");
if (!captureMethod || captureMethod->empty()) {
return failManifest(HushChatCaptureManifestError::MissingCaptureMethod);
}
auto handling = object.find("handling");
if (handling == object.end() || !handling->is_object()) {
return failManifest(HushChatCaptureManifestError::MissingHandling);
}
for (const char* flag : requiredCaptureHandlingFlags()) {
auto flagValue = handling->find(flag);
if (flagValue == handling->end()) return failManifest(HushChatCaptureManifestError::MissingHandlingFlag);
if (!flagValue->is_boolean() || !flagValue->get<bool>()) {
return failManifest(HushChatCaptureManifestError::HandlingFlagNotTrue);
}
++result.handling_flag_count;
}
result.no_sensitive_material_declared = true;
auto categories = object.find("categories");
if (categories == object.end() || !categories->is_array()) {
return failManifest(HushChatCaptureManifestError::MissingCategories);
}
auto findCategoryReport = [&](HushChatCompatibilityFixtureKind kind) -> HushChatCaptureManifestCategoryReport* {
for (auto& report : result.categories) {
if (report.kind == kind) return &report;
}
return nullptr;
};
for (const auto& category : *categories) {
if (!category.is_object()) return failManifest(HushChatCaptureManifestError::InvalidCategoryEntry);
auto kindText = readJsonString(category, "kind");
auto stagedFilename = readJsonString(category, "staged_filename");
auto categoryStatus = readJsonString(category, "status");
if (!kindText || kindText->empty() || !stagedFilename || stagedFilename->empty() ||
!categoryStatus || *categoryStatus != "ready") {
return failManifest(HushChatCaptureManifestError::InvalidCategoryEntry);
}
auto kind = parseFixtureKind(*kindText);
if (!kind) return failManifest(HushChatCaptureManifestError::UnknownCategory);
auto* report = findCategoryReport(*kind);
if (!report) return failManifest(HushChatCaptureManifestError::UnknownCategory);
if (report->declared) {
++result.duplicate_count;
return failManifest(HushChatCaptureManifestError::DuplicateCategory);
}
report->declared = true;
report->staged_filename = *stagedFilename;
++result.declared_count;
}
for (const auto& report : result.categories) {
if (!report.declared) ++result.missing_count;
}
if (result.missing_count > 0) return failManifest(HushChatCaptureManifestError::MissingRequiredCategory);
result.ok = true;
result.error = HushChatCaptureManifestError::None;
result.error_name = hushChatCaptureManifestErrorName(result.error);
return result;
}
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
const std::string& path,
bool featureEnabled)
{
if (!featureEnabled) {
HushChatCaptureManifestValidationResult result;
result.error = HushChatCaptureManifestError::FeatureDisabled;
result.error_name = hushChatCaptureManifestErrorName(result.error);
return result;
}
std::ifstream input(path);
if (!input.good()) {
HushChatCaptureManifestValidationResult result;
result.feature_enabled = true;
result.manifest_path = path;
result.error = HushChatCaptureManifestError::FileReadFailed;
result.error_name = hushChatCaptureManifestErrorName(result.error);
return result;
}
std::ostringstream buffer;
buffer << input.rdbuf();
auto result = validateHushChatCaptureManifest(buffer.str(), true);
result.manifest_path = path;
return result;
}
} // namespace dragonx::chat