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>
58 lines
2.9 KiB
C++
58 lines
2.9 KiB
C++
#pragma once
|
|
|
|
// DragonX Wallet - HushChat identity derivation.
|
|
//
|
|
// The DragonX-native chat identity is an X25519 (crypto_kx) keypair derived from a stable
|
|
// per-wallet secret via a domain-separated keyed BLAKE2b KDF. This deliberately does NOT
|
|
// use SDXL's UTF-8-hex-seed quirk (that quirk is only for the Phase-4 "import an existing
|
|
// SDXL identity" path). Interop is unaffected: identity derivation is local — peers only
|
|
// exchange public keys. See docs/_archive/contacts-chat-tab-plan-2026-07-05.md §5.6.
|
|
|
|
#include "chat_crypto.h" // ChatKeyPair
|
|
#include "chat_protocol.h" // hushChatFeatureEnabledAtBuild()
|
|
|
|
#include <cstddef>
|
|
#include <string>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
// Domain-separation label — used as the BLAKE2b key so a different app/version cannot
|
|
// derive the same identity from the same wallet secret. A char[] (not const char*) so its
|
|
// length is a compile-time constant for the KEYBYTES-bounds static_assert.
|
|
inline constexpr char kChatIdentityKdfContext[] = "DragonX-HushChat-Identity-v1";
|
|
inline constexpr std::size_t kChatIdentityKdfContextLen = sizeof(kChatIdentityKdfContext) - 1;
|
|
|
|
enum class ChatIdentityStatus {
|
|
Ready,
|
|
FeatureDisabled, // DRAGONX_ENABLE_CHAT off (or caller passed featureEnabled=false)
|
|
SecretUnavailable, // no stable secret (wallet locked / not open / empty)
|
|
DerivationFailed // libsodium init or KDF/keypair failure
|
|
};
|
|
|
|
const char* chatIdentityStatusName(ChatIdentityStatus status);
|
|
|
|
struct ChatIdentityResult {
|
|
ChatIdentityStatus status = ChatIdentityStatus::FeatureDisabled;
|
|
std::string public_key_hex; // 64 lowercase hex chars when Ready
|
|
const char* error_name = "FeatureDisabled"; // == chatIdentityStatusName(status)
|
|
};
|
|
|
|
// Pure, no-I/O derivation: hashes `stableSecret` (variable-length: a mnemonic on lite, a
|
|
// spending key on full-node) with the KDF context as the BLAKE2b key into a clean 32-byte
|
|
// crypto_kx seed, then crypto_kx_seed_keypair() into `outKeys`. Deterministic for a given
|
|
// secret. On any non-Ready result `outKeys` is left wiped. featureEnabled defaults to the
|
|
// build predicate but tests pass true to exercise the crypto in an OFF build.
|
|
//
|
|
// OWNERSHIP: `stableSecret` is BORROWED (const&) and is NOT wiped here — the caller owns it
|
|
// and MUST sodium_memzero its backing buffer after this returns. The per-variant provider
|
|
// that fetches the wallet secret (mnemonic / spending key) should hold it in a wipeable
|
|
// buffer, not a plain std::string literal, in production.
|
|
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
|
|
ChatKeyPair& outKeys,
|
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
|
|
|
// 64-char lowercase hex of the public key.
|
|
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys);
|
|
|
|
} // namespace dragonx::chat
|