#include "chat_fixture_tooling.h" #include #include #include #include #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; } std::optional hexNibble(unsigned char character) { if (character >= '0' && character <= '9') return static_cast(character - '0'); if (character >= 'a' && character <= 'f') return static_cast(character - 'a' + 10); if (character >= 'A' && character <= 'F') return static_cast(character - 'A' + 10); return std::nullopt; } std::optional 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::optional 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(); auto value = it->get(); if (value < 0) return std::nullopt; return static_cast(value); } std::optional parseFixtureDirection(const std::string& value) { if (value == "Incoming") return HushChatDecryptDirection::Incoming; if (value == "Outgoing") return HushChatDecryptDirection::Outgoing; return std::nullopt; } std::optional parseFixtureSessionKeySelection(const std::string& value) { if (value == "ClientRx") return HushChatSessionKeySelection::ClientRx; if (value == "ServerTx") return HushChatSessionKeySelection::ServerTx; return std::nullopt; } std::optional 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 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(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 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 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* 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; } 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(hex[index])); auto low = hexNibble(static_cast(hex[index + 1])); if (!high || !low) return failDecode(HushChatHexDecodeError::InvalidHex); result.bytes.push_back(static_cast((*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(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 { 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(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 hushChatRequiredCompatibilityFixtureKinds() { return { HushChatCompatibilityFixtureKind::IncomingMemo, HushChatCompatibilityFixtureKind::OutgoingMemo, HushChatCompatibilityFixtureKind::SeedPublicKeyProjection, HushChatCompatibilityFixtureKind::CorruptedAuthFailure, HushChatCompatibilityFixtureKind::ContactExclusion }; } HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist( const std::vector& 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& 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()) { 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