DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
157 lines
5.6 KiB
TypeScript
157 lines
5.6 KiB
TypeScript
/**
|
|
* 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)}`;
|
|
}
|