chore: initial commit for v0.0.1
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
This commit is contained in:
156
client-app/lib/crypto.ts
Normal file
156
client-app/lib/crypto.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user