import { describe, test, expect } from 'bun:test'; import { readFileSync } from 'fs'; import { join } from 'path'; import { SubtleCryptoProvider } from '@shade/crypto-web'; import { computeFingerprint, kdfRootKey, kdfChainKey, deriveInitialRootKey, } from '../src/index.js'; import { encodeEnvelope, decodeEnvelope } from '@shade/proto'; import type { RatchetMessage, ShadeEnvelope } from '../src/index.js'; const crypto = new SubtleCryptoProvider(); const VECTORS_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; } function loadVectors(name: string): any { return JSON.parse(readFileSync(join(VECTORS_DIR, name), 'utf-8')); } describe('Cross-platform test vectors', () => { test('HKDF vectors match', async () => { const { vectors } = loadVectors('hkdf.json'); for (const v of vectors) { const out = await crypto.hkdf( fromHex(v.ikm), fromHex(v.salt), new TextEncoder().encode(v.info), v.length, ); expect(hex(out)).toBe(v.output); } }); test('KDF chain vectors match', async () => { const { vectors } = loadVectors('kdf-chain.json'); const rootVec = vectors[0]; const rootResult = await kdfRootKey( crypto, fromHex(rootVec.rootKey), fromHex(rootVec.dhOutput), ); expect(hex(rootResult.newRootKey)).toBe(rootVec.newRootKey); expect(hex(rootResult.chainKey)).toBe(rootVec.chainKey); const chainVec = vectors[1]; const chainResult = await kdfChainKey(crypto, fromHex(chainVec.chainKey)); expect(hex(chainResult.newChainKey)).toBe(chainVec.newChainKey); expect(hex(chainResult.messageKey)).toBe(chainVec.messageKey); }); test('X3DH initial root key vectors match', async () => { const { vectors } = loadVectors('x3dh.json'); for (const v of vectors) { const rootKey = await deriveInitialRootKey( crypto, v.secrets.map((s: string) => fromHex(s)), ); expect(hex(rootKey)).toBe(v.rootKey); } }); test('Fingerprint vectors match', async () => { const { vectors } = loadVectors('fingerprint.json'); for (const v of vectors) { const fp = await computeFingerprint(crypto, fromHex(v.signingKey), fromHex(v.dhKey)); expect(fp).toBe(v.fingerprint); } }); test('Wire format vectors match', () => { const { vectors } = loadVectors('wire-format.json'); const v = vectors[0]; const msg: RatchetMessage = { dhPublicKey: fromHex(v.message.dhPublicKey), previousCounter: v.message.previousCounter, counter: v.message.counter, ciphertext: fromHex(v.message.ciphertext), nonce: fromHex(v.message.nonce), }; const envelope: ShadeEnvelope = { type: 'ratchet', content: msg, timestamp: 0, senderAddress: '', }; const encoded = encodeEnvelope(envelope); expect(hex(encoded)).toBe(v.encoded); // Also verify round-trip decode const decoded = decodeEnvelope(encoded); expect(decoded.type).toBe('ratchet'); const rm = decoded.content as RatchetMessage; expect(rm.counter).toBe(msg.counter); expect(hex(rm.ciphertext)).toBe(hex(msg.ciphertext)); }); });