import { describe, test, expect } from 'bun:test'; import { KeyManager } from '../src/crypto/key-manager.js'; import { DEFAULT_ARGON2ID, deriveMasterKeyArgon2id, type Argon2idParams, } from '../src/crypto/kdf.js'; const FAST_ARGON: Argon2idParams = { m: 256, t: 1, p: 1, dkLen: 32 }; function randBytes(n: number): Uint8Array { const b = new Uint8Array(n); globalThis.crypto.getRandomValues(b); return b; } describe('argon2id source', () => { const salt = new Uint8Array(16).fill(0x33); test('deriveMasterKeyArgon2id is deterministic', async () => { const a = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON); const b = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON); expect(a).toEqual(b); expect(a.length).toBe(32); }); test('different secret → different key', async () => { const a = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON); const b = await deriveMasterKeyArgon2id('1235', salt, FAST_ARGON); expect(a).not.toEqual(b); }); test('different salt → different key', async () => { const a = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON); const b = await deriveMasterKeyArgon2id('1234', new Uint8Array(16).fill(0x44), FAST_ARGON); expect(a).not.toEqual(b); }); test('rejects empty secret', async () => { await expect(deriveMasterKeyArgon2id('', salt, FAST_ARGON)).rejects.toThrow(/non-empty/); }); test('rejects too-short salt', async () => { await expect(deriveMasterKeyArgon2id('p', new Uint8Array(8), FAST_ARGON)) .rejects.toThrow(/at least 16/); }); test('KeyManager.open opens with argon2id source', async () => { const km = await KeyManager.open({ kind: 'argon2id', secret: '123456', salt, params: FAST_ARGON, }); expect(km.fieldKey('t', 'c').length).toBe(32); km.destroy(); }); test('DEFAULT_ARGON2ID is exposed and sensible', () => { expect(DEFAULT_ARGON2ID.dkLen).toBe(32); expect(DEFAULT_ARGON2ID.m).toBeGreaterThanOrEqual(8 * 1024); expect(DEFAULT_ARGON2ID.t).toBeGreaterThanOrEqual(1); }); test('accepts Uint8Array secret', async () => { const secretBytes = new TextEncoder().encode('1234'); const a = await deriveMasterKeyArgon2id(secretBytes, salt, FAST_ARGON); const b = await deriveMasterKeyArgon2id('1234', salt, FAST_ARGON); expect(a).toEqual(b); }); }); describe('composite source — multi-factor unlock', () => { const pwSalt = new Uint8Array(16).fill(0x11); const pinSalt = new Uint8Array(16).fill(0x22); const FAST_SCRYPT = { N: 1 << 10, r: 8, p: 1, dkLen: 32 }; function pwSource(passphrase: string) { return { kind: 'passphrase' as const, passphrase, salt: pwSalt, params: FAST_SCRYPT }; } function pinSource(secret: string) { return { kind: 'argon2id' as const, secret, salt: pinSalt, params: FAST_ARGON }; } test('same factors → same masterKey', async () => { const a = await KeyManager.open({ kind: 'composite', sources: [pwSource('correct horse'), pinSource('1234')], }); const b = await KeyManager.open({ kind: 'composite', sources: [pwSource('correct horse'), pinSource('1234')], }); expect(a.storageKeyFingerprint()).toEqual(b.storageKeyFingerprint()); a.destroy(); b.destroy(); }); test('wrong PIN → different masterKey (same shape as wrong-passphrase)', async () => { const right = await KeyManager.open({ kind: 'composite', sources: [pwSource('correct horse'), pinSource('1234')], }); const wrongPin = await KeyManager.open({ kind: 'composite', sources: [pwSource('correct horse'), pinSource('9999')], }); expect(right.storageKeyFingerprint()).not.toEqual(wrongPin.storageKeyFingerprint()); right.destroy(); wrongPin.destroy(); }); test('wrong passphrase → different masterKey', async () => { const right = await KeyManager.open({ kind: 'composite', sources: [pwSource('correct horse'), pinSource('1234')], }); const wrongPwd = await KeyManager.open({ kind: 'composite', sources: [pwSource('wrong horse'), pinSource('1234')], }); expect(right.storageKeyFingerprint()).not.toEqual(wrongPwd.storageKeyFingerprint()); right.destroy(); wrongPwd.destroy(); }); test('order is significant by design', async () => { const ab = await KeyManager.open({ kind: 'composite', sources: [pwSource('horse'), pinSource('1234')], }); const ba = await KeyManager.open({ kind: 'composite', sources: [pinSource('1234'), pwSource('horse')], }); expect(ab.storageKeyFingerprint()).not.toEqual(ba.storageKeyFingerprint()); ab.destroy(); ba.destroy(); }); test('explicit info string changes masterKey (domain separation)', async () => { const a = await KeyManager.open({ kind: 'composite', sources: [pwSource('horse'), pinSource('1234')], }); const b = await KeyManager.open({ kind: 'composite', sources: [pwSource('horse'), pinSource('1234')], info: 'my-app-v1', }); expect(a.storageKeyFingerprint()).not.toEqual(b.storageKeyFingerprint()); a.destroy(); b.destroy(); }); test('rejects empty source list', async () => { await expect(KeyManager.open({ kind: 'composite', sources: [] })) .rejects.toThrow(/at least one/); }); test('rejects nested composite', async () => { await expect(KeyManager.open({ kind: 'composite', sources: [ { kind: 'composite', sources: [pwSource('a')] }, pinSource('1234'), ], })).rejects.toThrow(/cannot be nested/); }); test('composite of three sources works', async () => { const km = await KeyManager.open({ kind: 'composite', sources: [ pwSource('horse'), pinSource('1234'), { kind: 'injected', key: randBytes(32) }, ], }); expect(km.fieldKey('t', 'c').length).toBe(32); km.destroy(); }); });