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>
66 lines
2.9 KiB
C++
66 lines
2.9 KiB
C++
#pragma once
|
|
|
|
// DragonX Wallet - HushChat crypto primitives.
|
|
//
|
|
// crypto_kx (X25519) session-key agreement + crypto_secretstream_xchacha20poly1305
|
|
// message encryption, byte-exact per the HushChat wire format so DragonX interoperates
|
|
// with SilentDragonXLite. See docs/_archive/contacts-chat-phase1-detail-2026-07-05.md
|
|
// (Appendix A.3/A.4). Pure crypto — no gating, no I/O; the feature gate lives at the
|
|
// service layer. NEVER logs plaintext, ciphertext, keys, or session material.
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <string>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
// crypto_kx key sizes (== crypto_kx_PUBLICKEYBYTES / SECRETKEYBYTES == 32).
|
|
constexpr std::size_t kChatKeyBytes = 32;
|
|
|
|
using ChatPublicKey = std::array<unsigned char, kChatKeyBytes>;
|
|
using ChatSecretKey = std::array<unsigned char, kChatKeyBytes>;
|
|
|
|
struct ChatKeyPair {
|
|
ChatPublicKey public_key{};
|
|
ChatSecretKey secret_key{};
|
|
};
|
|
|
|
enum class ChatCryptoStatus {
|
|
Ok,
|
|
SodiumInitFailed,
|
|
BadPeerKey, // peer public-key hex missing / wrong length / not hex
|
|
BadHeaderHex, // secretstream header hex missing / wrong length / not hex
|
|
BadCiphertextHex, // ciphertext hex malformed
|
|
CiphertextTooShort, // ciphertext shorter than the auth tag
|
|
SessionKeyFailed, // crypto_kx_*_session_keys rejected the peer key
|
|
EncryptFailed,
|
|
DecryptFailed // init_pull / pull / tag mismatch — the single neutral auth failure
|
|
};
|
|
|
|
const char* chatCryptoStatusName(ChatCryptoStatus status);
|
|
|
|
// Encrypt `plaintext` addressed to peer `peerPublicKeyHex` (64 lowercase hex chars).
|
|
// Sender takes the crypto_kx "server" role (server_tx), matching SDXL's send path.
|
|
// Outputs the secretstream header hex (the memo "e" field) and the ciphertext hex
|
|
// (the payload memo). Returns Ok on success.
|
|
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
|
|
const std::string& peerPublicKeyHex,
|
|
const std::string& plaintext,
|
|
std::string& outStreamHeaderHex,
|
|
std::string& outCiphertextHex);
|
|
|
|
// Decrypt an incoming message addressed to us from peer `peerPublicKeyHex`.
|
|
// Receiver takes the crypto_kx "client" role (client_rx), matching SDXL's receive path.
|
|
// Requires the memo "e" (streamHeaderHex) and the payload ciphertext hex. Enforces the
|
|
// Poly1305 auth tag AND that the stream tag is TAG_FINAL. Returns Ok + fills outPlaintext.
|
|
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
|
|
const std::string& peerPublicKeyHex,
|
|
const std::string& streamHeaderHex,
|
|
const std::string& ciphertextHex,
|
|
std::string& outPlaintext);
|
|
|
|
// Zero both key arrays (call when discarding an identity's keys).
|
|
void wipeChatKeyPair(ChatKeyPair& keys);
|
|
|
|
} // namespace dragonx::chat
|