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>
1855 lines
78 KiB
C++
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
|