/** * Cryptographic operations for DChain messenger. * * Ed25519 — transaction signing (via TweetNaCl sign) * X25519 — Diffie-Hellman key exchange for NaCl box * NaCl box — authenticated encryption for relay messages */ import nacl from 'tweetnacl'; import { decodeUTF8, encodeUTF8 } from 'tweetnacl-util'; import { getRandomBytes } from 'expo-crypto'; import type { KeyFile } from './types'; // ─── PRNG ───────────────────────────────────────────────────────────────────── // TweetNaCl looks for window.crypto which doesn't exist in React Native/Hermes. // Wire nacl to expo-crypto which uses the platform's secure RNG natively. nacl.setPRNG((output: Uint8Array, length: number) => { const bytes = getRandomBytes(length); for (let i = 0; i < length; i++) output[i] = bytes[i]; }); // ─── Helpers ────────────────────────────────────────────────────────────────── export function hexToBytes(hex: string): Uint8Array { if (hex.length % 2 !== 0) throw new Error('odd hex length'); const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return bytes; } export function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); } // ─── Key generation ─────────────────────────────────────────────────────────── /** * Generate a new identity: Ed25519 signing keys + X25519 encryption keys. * Returns a KeyFile compatible with the Go node format. */ export function generateKeyFile(): KeyFile { // Ed25519 for signing / blockchain identity const signKP = nacl.sign.keyPair(); // X25519 for NaCl box encryption // nacl.box.keyPair() returns Curve25519 keys const boxKP = nacl.box.keyPair(); return { pub_key: bytesToHex(signKP.publicKey), priv_key: bytesToHex(signKP.secretKey), x25519_pub: bytesToHex(boxKP.publicKey), x25519_priv: bytesToHex(boxKP.secretKey), }; } // ─── NaCl box encryption ────────────────────────────────────────────────────── /** * Encrypt a plaintext message using NaCl box. * Sender uses their X25519 secret key + recipient's X25519 public key. * Returns { nonce, ciphertext } as hex strings. */ export function encryptMessage( plaintext: string, senderSecretHex: string, recipientPubHex: string, ): { nonce: string; ciphertext: string } { const nonce = nacl.randomBytes(nacl.box.nonceLength); const message = decodeUTF8(plaintext); const secretKey = hexToBytes(senderSecretHex); const publicKey = hexToBytes(recipientPubHex); const box = nacl.box(message, nonce, publicKey, secretKey); return { nonce: bytesToHex(nonce), ciphertext: bytesToHex(box), }; } /** * Decrypt a NaCl box. * Recipient uses their X25519 secret key + sender's X25519 public key. */ export function decryptMessage( ciphertextHex: string, nonceHex: string, senderPubHex: string, recipientSecHex: string, ): string | null { try { const ciphertext = hexToBytes(ciphertextHex); const nonce = hexToBytes(nonceHex); const senderPub = hexToBytes(senderPubHex); const secretKey = hexToBytes(recipientSecHex); const plain = nacl.box.open(ciphertext, nonce, senderPub, secretKey); if (!plain) return null; return encodeUTF8(plain); } catch { return null; } } // ─── Ed25519 signing ────────────────────────────────────────────────────────── /** * Sign arbitrary data with the Ed25519 private key. * Returns signature as hex. */ export function sign(data: Uint8Array, privKeyHex: string): string { const secretKey = hexToBytes(privKeyHex); const sig = nacl.sign.detached(data, secretKey); return bytesToHex(sig); } /** * Sign arbitrary data with the Ed25519 private key. * Returns signature as base64 — this is the format the Go blockchain node * expects ([]byte fields are base64 in JSON). */ export function signBase64(data: Uint8Array, privKeyHex: string): string { const secretKey = hexToBytes(privKeyHex); const sig = nacl.sign.detached(data, secretKey); return bytesToBase64(sig); } /** Encode bytes as base64. Works on Hermes (btoa is available since RN 0.71). */ export function bytesToBase64(bytes: Uint8Array): string { let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** * Verify an Ed25519 signature. */ export function verify(data: Uint8Array, sigHex: string, pubKeyHex: string): boolean { try { return nacl.sign.detached.verify(data, hexToBytes(sigHex), hexToBytes(pubKeyHex)); } catch { return false; } } // ─── Address helpers ────────────────────────────────────────────────────────── /** Truncate a long hex address for display: 8...8 */ export function shortAddr(hex: string, chars = 8): string { if (hex.length <= chars * 2 + 3) return hex; return `${hex.slice(0, chars)}…${hex.slice(-chars)}`; }