// Krux KEF tests using Bun test runner import { describe, test, expect } from "bun:test"; import { encryptToKrux, decryptFromKrux, hexToBytes, bytesToHex, wrap, unwrap, KruxCipher } from './krux'; import { getWalletFingerprint } from "./bip32"; describe('Krux KEF Implementation', () => { // Test basic hex conversion test('hexToBytes and bytesToHex roundtrip', () => { const original = 'Hello, World!'; const bytes = new TextEncoder().encode(original); const hex = bytesToHex(bytes); const back = hexToBytes(hex); expect(new TextDecoder().decode(back)).toBe(original); }); test('hexToBytes handles KEF: prefix', () => { const hex = '48656C6C6F'; const withPrefix = `KEF:${hex}`; const bytes1 = hexToBytes(hex); const bytes2 = hexToBytes(withPrefix); expect(bytes2).toEqual(bytes1); }); test('hexToBytes rejects invalid hex', () => { expect(() => hexToBytes('12345')).toThrow('Hex string must have even length'); expect(() => hexToBytes('12345G')).toThrow('Invalid hex string'); }); // Test wrap/unwrap test('wrap and unwrap roundtrip', () => { const label = 'Test Label'; const version = 20; const iterations = 200000; const payload = new TextEncoder().encode('test payload'); const wrapped = wrap(label, version, iterations, payload); const unwrapped = unwrap(wrapped); expect(unwrapped.label).toBe(label); expect(unwrapped.version).toBe(version); expect(unwrapped.iterations).toBe(iterations); expect(unwrapped.payload).toEqual(payload); }); test('wrap rejects label too long', () => { const longLabel = 'a'.repeat(253); // 253 > 252 max const payload = new Uint8Array([1, 2, 3]); expect(() => wrap(longLabel, 20, 10000, payload)) .toThrow('Label too long'); }); test('wrap accepts empty label', () => { const payload = new Uint8Array([1, 2, 3]); const wrapped = wrap('', 20, 10000, payload); const unwrapped = unwrap(wrapped); expect(unwrapped.label).toBe(''); expect(unwrapped.version).toBe(20); expect(unwrapped.iterations).toBe(10000); expect(unwrapped.payload).toEqual(payload); }); test('unwrap rejects invalid envelope', () => { expect(() => unwrap(new Uint8Array([1, 2, 3]))).toThrow('Invalid KEF envelope: too short'); // Label length too large (253 > 252) expect(() => unwrap(new Uint8Array([253, 20, 0, 0, 100]))).toThrow('Invalid label length'); // Empty label (lenId=0) is valid, but need enough data for version+iterations // Create a valid envelope with empty label: [0, version, iter1, iter2, iter3, payload...] const emptyLabelEnvelope = new Uint8Array([0, 20, 0, 0, 100, 1, 2, 3]); const unwrapped = unwrap(emptyLabelEnvelope); expect(unwrapped.label).toBe(''); expect(unwrapped.version).toBe(20); }); // Test encryption/decryption test('encryptToKrux and decryptFromKrux roundtrip', async () => { const mnemonic = 'test test test test test test test test test test test junk'; const passphrase = 'secure-passphrase'; const encrypted = await encryptToKrux({ mnemonic, passphrase, }); const expectedLabel = getWalletFingerprint(mnemonic); expect(encrypted.kefBase43).toMatch(/^[0-9A-Z$*+-\./:]+$/); // Check Base43 format expect(encrypted.label).toBe(expectedLabel); expect(encrypted.iterations).toBe(100000); expect(encrypted.version).toBe(20); const decrypted = await decryptFromKrux({ kefData: encrypted.kefBase43, // Use kefBase43 for decryption passphrase, }); expect(decrypted.mnemonic).toBe(mnemonic); expect(decrypted.label).toBe(expectedLabel); expect(decrypted.iterations).toBe(100000); expect(decrypted.version).toBe(20); }); test('encryptToKrux requires passphrase', async () => { await expect(encryptToKrux({ mnemonic: 'test', passphrase: '', })).rejects.toThrow('Passphrase is required'); }); test('decryptFromKrux requires passphrase', async () => { await expect(decryptFromKrux({ kefData: '123456', passphrase: '', })).rejects.toThrow('Invalid Krux data: Not a valid Hex or Base43 string.'); // Updated error message }); test('wrong passphrase fails decryption', async () => { const mnemonic = 'test mnemonic'; const passphrase = 'correct-passphrase'; const encrypted = await encryptToKrux({ mnemonic, passphrase, }); await expect(decryptFromKrux({ kefData: encrypted.kefBase43, // Use kefBase43 for decryption passphrase: 'wrong-passphrase', })).rejects.toThrow(/Krux decryption failed/); }); // Test KruxCipher class directly test('KruxCipher encrypt/decrypt roundtrip', async () => { const cipher = new KruxCipher('passphrase', new TextEncoder().encode('salt'), 10000); const plaintext = new TextEncoder().encode('secret message'); const encrypted = await cipher.encrypt(plaintext); const decrypted = await cipher.decrypt(encrypted, 20); expect(new TextDecoder().decode(decrypted)).toBe('secret message'); }); test('KruxCipher rejects unsupported version', async () => { const cipher = new KruxCipher('passphrase', new TextEncoder().encode('salt'), 10000); const plaintext = new Uint8Array([1, 2, 3]); await expect(cipher.encrypt(plaintext, 99)).rejects.toThrow('Unsupported KEF version'); await expect(cipher.decrypt(new Uint8Array(50), 99)).rejects.toThrow('Unsupported KEF version'); // Changed error message }); test('KruxCipher rejects short payload', async () => { const cipher = new KruxCipher('passphrase', new TextEncoder().encode('salt'), 10000); // Version 20: IV (12) + auth (4) = 16 bytes minimum const shortPayload = new Uint8Array(15); // Too short for IV + GCM tag (needs at least 16) await expect(cipher.decrypt(shortPayload, 20)).rejects.toThrow('Payload too short for AES-GCM'); }); test('iterations scaling works correctly', () => { const label = 'Test'; const version = 20; const payload = new TextEncoder().encode('test payload'); const wrapped1 = wrap(label, version, 200000, payload); expect(wrapped1[6]).toBe(0); expect(wrapped1[7]).toBe(0); expect(wrapped1[8]).toBe(20); const wrapped2 = wrap(label, version, 10001, payload); const iterStart = 2 + label.length; const iters = (wrapped2[iterStart] << 16) | (wrapped2[iterStart + 1] << 8) | wrapped2[iterStart + 2]; expect(iters).toBe(10001); }); // New test case for user-provided KEF string - this one already uses base43Decode test('should correctly decrypt the user-provided KEF string', async () => { const kefData = "1334+HGXM$F8PPOIRNHX0.R*:SBMHK$X88LX$*/Y417R/6S1ZQOB2LHM-L+4T1YQVU:B*CKGXONP7:Y/R-B*:$R8FK"; const passphrase = "aaa"; const expectedMnemonic = "differ release beauty fresh tortoise usage curtain spoil october town embrace ridge rough reject cabin snap glimpse enter book coach green lonely hundred mercy"; const result = await decryptFromKrux({ kefData, passphrase }); expect(result.mnemonic).toBe(expectedMnemonic); }); });