#!/usr/bin/env bun /** * Generate cross-platform test vectors from the TypeScript implementation. * * The output JSON files are loaded by BOTH the TypeScript and Kotlin test * suites. Any divergence between platforms fails CI immediately. * * Usage: bun run scripts/generate-vectors.ts */ import { writeFileSync } from 'fs'; import { join } from 'path'; import { SubtleCryptoProvider, MemoryStorage } from '../packages/shade-crypto-web/src/index.js'; import { computeFingerprint } from '../packages/shade-core/src/fingerprint.js'; import { kdfChainKey, kdfRootKey, deriveInitialRootKey } from '../packages/shade-core/src/keys.js'; import { encodeEnvelope, decodeEnvelope } from '../packages/shade-proto/src/index.js'; import type { ShadeEnvelope, RatchetMessage } from '../packages/shade-core/src/index.js'; const crypto = new SubtleCryptoProvider(); const OUT_DIR = join(import.meta.dir, '..', 'test-vectors'); function hex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); } function fromHex(str: string): Uint8Array { const bytes = new Uint8Array(str.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(str.substring(i * 2, i * 2 + 2), 16); } return bytes; } interface Vector { description: string; [key: string]: any; } // ─── HKDF vectors ─────────────────────────────────────────── async function generateHkdfVectors(): Promise { const vectors: Vector[] = []; // Known inputs → expected outputs const cases = [ { ikm: '01'.repeat(32), salt: '02'.repeat(32), info: 'test', length: 32 }, { ikm: 'ab'.repeat(32), salt: '00'.repeat(32), info: 'ShadeRootRatchet', length: 64 }, { ikm: 'cd'.repeat(32), salt: '00'.repeat(32), info: 'ShadeX3DH', length: 32 }, ]; for (const c of cases) { const out = await crypto.hkdf( fromHex(c.ikm), fromHex(c.salt), new TextEncoder().encode(c.info), c.length, ); vectors.push({ description: `HKDF-SHA256 with ikm=${c.ikm.slice(0, 8)}... info="${c.info}"`, ikm: c.ikm, salt: c.salt, info: c.info, length: c.length, output: hex(out), }); } return vectors; } // ─── KDF chain vectors ───────────────────────────────────── async function generateKdfChainVectors(): Promise { const rootKey = new Uint8Array(32).fill(0x11); const dhOutput = new Uint8Array(32).fill(0x22); const rootResult = await kdfRootKey(crypto, rootKey, dhOutput); const chainKey = new Uint8Array(32).fill(0x33); const chainResult = await kdfChainKey(crypto, chainKey); return [ { description: 'Root key ratchet: kdfRootKey', rootKey: hex(rootKey), dhOutput: hex(dhOutput), newRootKey: hex(rootResult.newRootKey), chainKey: hex(rootResult.chainKey), }, { description: 'Chain key ratchet: kdfChainKey', chainKey: hex(chainKey), newChainKey: hex(chainResult.newChainKey), messageKey: hex(chainResult.messageKey), }, ]; } // ─── X3DH initial root key ───────────────────────────────── async function generateX3DHVectors(): Promise { const secrets = [ new Uint8Array(32).fill(0xaa), new Uint8Array(32).fill(0xbb), new Uint8Array(32).fill(0xcc), ]; const rootKey3 = await deriveInitialRootKey(crypto, secrets); const secrets4 = [...secrets, new Uint8Array(32).fill(0xdd)]; const rootKey4 = await deriveInitialRootKey(crypto, secrets4); return [ { description: 'X3DH initial root key with 3 DH outputs (no one-time prekey)', secrets: secrets.map(hex), rootKey: hex(rootKey3), }, { description: 'X3DH initial root key with 4 DH outputs (with one-time prekey)', secrets: secrets4.map(hex), rootKey: hex(rootKey4), }, ]; } // ─── Fingerprint vectors ─────────────────────────────────── async function generateFingerprintVectors(): Promise { const cases = [ { sig: '01'.repeat(32), dh: '02'.repeat(32) }, { sig: 'ab'.repeat(32), dh: 'cd'.repeat(32) }, ]; const vectors: Vector[] = []; for (const c of cases) { const fp = await computeFingerprint(crypto, fromHex(c.sig), fromHex(c.dh)); vectors.push({ description: `Fingerprint for signing=${c.sig.slice(0, 8)}... dh=${c.dh.slice(0, 8)}...`, signingKey: c.sig, dhKey: c.dh, fingerprint: fp, }); } return vectors; } // ─── Wire format vectors ─────────────────────────────────── async function generateWireFormatVectors(): Promise { // Deterministic inputs const ratchetMsg: RatchetMessage = { dhPublicKey: new Uint8Array(32).fill(0x11), previousCounter: 42, counter: 7, ciphertext: new Uint8Array(16).fill(0x22), nonce: new Uint8Array(12).fill(0x33), }; const envelopeRatchet: ShadeEnvelope = { type: 'ratchet', content: ratchetMsg, timestamp: 0, senderAddress: '', }; const bytesRatchet = encodeEnvelope(envelopeRatchet); return [ { description: 'Wire format: RatchetMessage encoding', message: { dhPublicKey: hex(ratchetMsg.dhPublicKey), previousCounter: ratchetMsg.previousCounter, counter: ratchetMsg.counter, ciphertext: hex(ratchetMsg.ciphertext), nonce: hex(ratchetMsg.nonce), }, encoded: hex(bytesRatchet), }, ]; } async function main() { console.log('Generating cross-platform test vectors…'); const files: Array<[string, any]> = [ ['hkdf.json', { vectors: await generateHkdfVectors() }], ['kdf-chain.json', { vectors: await generateKdfChainVectors() }], ['x3dh.json', { vectors: await generateX3DHVectors() }], ['fingerprint.json', { vectors: await generateFingerprintVectors() }], ['wire-format.json', { vectors: await generateWireFormatVectors() }], ]; for (const [name, data] of files) { const path = join(OUT_DIR, name); writeFileSync(path, JSON.stringify(data, null, 2) + '\n'); console.log(` ✓ ${name} (${data.vectors.length} vectors)`); } console.log('Done.'); } main().catch((err) => { console.error(err); process.exit(1); });