import { describe, test, expect } from 'bun:test'; import { SubtleCryptoProvider, MemoryStorage } from '@shade/crypto-web'; import { ShadeSessionManager, computeFingerprint, shortFingerprint } from '../src/index.js'; const crypto = new SubtleCryptoProvider(); describe('Fingerprints (safety numbers)', () => { test('computeFingerprint produces 12 groups of 5 digits', async () => { const sig = crypto.randomBytes(32); const dh = crypto.randomBytes(32); const fp = await computeFingerprint(crypto, sig, dh); const groups = fp.split(' '); expect(groups.length).toBe(12); for (const g of groups) { expect(g).toMatch(/^\d{5}$/); } }); test('same input produces same fingerprint (deterministic)', async () => { const sig = new Uint8Array(32).fill(0xab); const dh = new Uint8Array(32).fill(0xcd); const fp1 = await computeFingerprint(crypto, sig, dh); const fp2 = await computeFingerprint(crypto, sig, dh); expect(fp1).toBe(fp2); }); test('different identities produce different fingerprints', async () => { const sig1 = crypto.randomBytes(32); const dh1 = crypto.randomBytes(32); const sig2 = crypto.randomBytes(32); const dh2 = crypto.randomBytes(32); const fp1 = await computeFingerprint(crypto, sig1, dh1); const fp2 = await computeFingerprint(crypto, sig2, dh2); expect(fp1).not.toBe(fp2); }); test('shortFingerprint is 4 groups', () => { const full = '11111 22222 33333 44444 55555 66666 77777 88888 99999 11111 22222 33333'; expect(shortFingerprint(full)).toBe('11111 22222 33333 44444'); }); }); describe('ShadeSessionManager fingerprints', () => { test('getIdentityFingerprint returns stable value', async () => { const storage = new MemoryStorage(); const mgr = new ShadeSessionManager(crypto, storage); await mgr.initialize(); const fp1 = await mgr.getIdentityFingerprint(); const fp2 = await mgr.getIdentityFingerprint(); expect(fp1).toBe(fp2); expect(fp1.split(' ').length).toBe(12); }); test('fingerprint persists across SessionManager instances', async () => { const storage = new MemoryStorage(); const mgr1 = new ShadeSessionManager(crypto, storage); await mgr1.initialize(); const fp1 = await mgr1.getIdentityFingerprint(); const mgr2 = new ShadeSessionManager(crypto, storage); await mgr2.initialize(); const fp2 = await mgr2.getIdentityFingerprint(); expect(fp1).toBe(fp2); }); test('different identities have different fingerprints', async () => { const alice = new ShadeSessionManager(crypto, new MemoryStorage()); const bob = new ShadeSessionManager(crypto, new MemoryStorage()); await alice.initialize(); await bob.initialize(); const fpA = await alice.getIdentityFingerprint(); const fpB = await bob.getIdentityFingerprint(); expect(fpA).not.toBe(fpB); }); test('getShortFingerprint returns 4 groups', async () => { const mgr = new ShadeSessionManager(crypto, new MemoryStorage()); await mgr.initialize(); const short = await mgr.getShortFingerprint(); expect(short.split(' ').length).toBe(4); }); }); describe('Prekey stock management', () => { test('ensurePreKeyStock generates keys when below min', async () => { const mgr = new ShadeSessionManager(crypto, new MemoryStorage()); await mgr.initialize(); // Start with 0 OTPKs const generated = await mgr.ensurePreKeyStock(5, 10); expect(generated).toBe(10); }); test('ensurePreKeyStock does nothing when above min', async () => { const mgr = new ShadeSessionManager(crypto, new MemoryStorage()); await mgr.initialize(); await mgr.generateOneTimePreKeys(10); const generated = await mgr.ensurePreKeyStock(5, 10); expect(generated).toBe(0); }); test('ensurePreKeyStock tops up to target', async () => { const mgr = new ShadeSessionManager(crypto, new MemoryStorage()); await mgr.initialize(); await mgr.generateOneTimePreKeys(3); // Below min of 5, should generate enough to reach target of 20 const generated = await mgr.ensurePreKeyStock(5, 20); expect(generated).toBe(17); }); }); describe('Session reset + identity change', () => { async function setupPair() { const alice = new ShadeSessionManager(crypto, new MemoryStorage()); const bob = new ShadeSessionManager(crypto, new MemoryStorage()); await alice.initialize(); await bob.initialize(); const otpks = await bob.generateOneTimePreKeys(5); const bundle = await bob.createPreKeyBundle(); bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey }; await alice.initSessionFromBundle('bob', bundle); return { alice, bob }; } test('resetSession removes the session', async () => { const { alice } = await setupPair(); // Encrypt once to confirm session exists await alice.encrypt('bob', 'hello'); await alice.resetSession('bob'); expect(alice.encrypt('bob', 'next')).rejects.toThrow('No session'); }); test('acceptIdentityChange updates pinned trust', async () => { const { alice, bob } = await setupPair(); // Alice verifies Bob's current identity const bobId = bob.getPublicIdentity(); expect(await alice.verifyRemoteIdentity('bob', bobId.dhKey)).toBe(true); // Different key is rejected const fakeKey = crypto.randomBytes(32); expect(await alice.verifyRemoteIdentity('bob', fakeKey)).toBe(false); // Accept the fake key as the new Bob await alice.acceptIdentityChange('bob', fakeKey); // Now the fake key is trusted, old one isn't expect(await alice.verifyRemoteIdentity('bob', fakeKey)).toBe(true); expect(await alice.verifyRemoteIdentity('bob', bobId.dhKey)).toBe(false); // Session was also removed expect(alice.encrypt('bob', 'test')).rejects.toThrow('No session'); }); });