Phase 1 crypto foundation (gated by DRAGONX_ENABLE_CHAT; the pure primitives are always compiled + unit-tested, the feature gate lives at the future service layer). - chat_identity: derive the DragonX-native X25519 (crypto_kx) identity from a stable per-wallet secret via a domain-separated keyed BLAKE2b KDF (crypto_generichash, context "DragonX-HushChat-Identity-v1") into a clean 32-byte crypto_kx seed. Deterministic; skips SDXL's UTF-8-hex-seed quirk (that's the Phase-4 import path). Per §5.6. - chat_crypto: encryptOutgoing (server_tx) / decryptIncoming (client_rx) via crypto_secretstream_xchacha20poly1305, byte-exact per Appendix A.3/A.4 so DragonX interoperates with SilentDragonXLite. Single chunk, TAG_FINAL; decrypt enforces both the Poly1305 tag and TAG_FINAL (stricter than SDXL). Every session key / seed / stream state / plaintext scratch is sodium_memzero'd on all paths; no secret is ever logged. - Tests: encrypt->decrypt round-trip (incl. empty + UTF-8), identity determinism, feature-gate + empty-secret handling, and malformed/tampered/ wrong-key inputs all fail safely. Adversarially security-reviewed (3 lenses: secret hygiene, crypto correctness, SDXL wire-interop) — confirmed byte-compatible with the SDXL reference in both directions. Fixes from review: check the secretstream_push return before reporting Ok; allow the empty-plaintext ciphertext (== ABYTES) so encrypt/decrypt round-trip symmetrically; document the caller-owns-and-wipes secret contract. Interop caveat: round-trip proves self-consistency; a real captured SDXL message is still needed to prove wire-compat end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.9 KiB
C++
71 lines
2.9 KiB
C++
// DragonX Wallet - HushChat identity derivation (implementation).
|
|
|
|
#include "chat_identity.h"
|
|
|
|
#include <sodium.h>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
// The KDF context is used as the BLAKE2b key, so its length must sit within the primitive's
|
|
// key bounds.
|
|
static_assert(kChatIdentityKdfContextLen >= crypto_generichash_KEYBYTES_MIN,
|
|
"chat identity KDF context is shorter than BLAKE2b's minimum key length");
|
|
static_assert(kChatIdentityKdfContextLen <= crypto_generichash_KEYBYTES_MAX,
|
|
"chat identity KDF context is longer than BLAKE2b's maximum key length");
|
|
|
|
const char* chatIdentityStatusName(ChatIdentityStatus status) {
|
|
switch (status) {
|
|
case ChatIdentityStatus::Ready: return "Ready";
|
|
case ChatIdentityStatus::FeatureDisabled: return "FeatureDisabled";
|
|
case ChatIdentityStatus::SecretUnavailable: return "SecretUnavailable";
|
|
case ChatIdentityStatus::DerivationFailed: return "DerivationFailed";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys) {
|
|
char hex[crypto_kx_PUBLICKEYBYTES * 2 + 1];
|
|
sodium_bin2hex(hex, sizeof hex, keys.public_key.data(), keys.public_key.size());
|
|
return std::string(hex);
|
|
}
|
|
|
|
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
|
|
ChatKeyPair& outKeys,
|
|
bool featureEnabled) {
|
|
ChatIdentityResult result;
|
|
auto finish = [&result](ChatIdentityStatus status) -> ChatIdentityResult {
|
|
result.status = status;
|
|
result.error_name = chatIdentityStatusName(status);
|
|
return result;
|
|
};
|
|
|
|
if (!featureEnabled) return finish(ChatIdentityStatus::FeatureDisabled);
|
|
if (stableSecret.empty()) return finish(ChatIdentityStatus::SecretUnavailable);
|
|
if (sodium_init() < 0) return finish(ChatIdentityStatus::DerivationFailed);
|
|
|
|
unsigned char kxSeed[crypto_kx_SEEDBYTES];
|
|
static_assert(sizeof(kxSeed) == kChatKeyBytes, "kx seed size mismatch");
|
|
|
|
const int hashStatus = crypto_generichash(
|
|
kxSeed, sizeof kxSeed,
|
|
reinterpret_cast<const unsigned char*>(stableSecret.data()), stableSecret.size(),
|
|
reinterpret_cast<const unsigned char*>(kChatIdentityKdfContext), kChatIdentityKdfContextLen);
|
|
if (hashStatus != 0) {
|
|
sodium_memzero(kxSeed, sizeof kxSeed);
|
|
return finish(ChatIdentityStatus::DerivationFailed);
|
|
}
|
|
|
|
const int keypairStatus =
|
|
crypto_kx_seed_keypair(outKeys.public_key.data(), outKeys.secret_key.data(), kxSeed);
|
|
sodium_memzero(kxSeed, sizeof kxSeed); // wipe the seed immediately, success or failure
|
|
if (keypairStatus != 0) {
|
|
wipeChatKeyPair(outKeys);
|
|
return finish(ChatIdentityStatus::DerivationFailed);
|
|
}
|
|
|
|
result.public_key_hex = chatIdentityPublicKeyHex(outKeys);
|
|
return finish(ChatIdentityStatus::Ready);
|
|
}
|
|
|
|
} // namespace dragonx::chat
|