import { describe, expect, test } from 'bun:test'; import { generateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { AezeedError, birthdayToDate, decipherMnemonic, isAezeedPhrase, mnemonicToCipherSeedBytes, } from './cipher-seed'; // Fixtures were produced by the `aezeed` npm package (an INDEPENDENT implementation, different author // and codebase, itself matching lnd/aezeed). Deterministic entropy and salt make them reproducible, so // agreement here is genuine cross-implementation evidence rather than this port agreeing with itself. // // entropy 000102...0f, salt 0102030405, internalVersion 0, birthday 5593 const ENTROPY_HEX = '000102030405060708090a0b0c0d0e0f'; const DEFAULT_PW = 'about wisdom spawn awkward catalog large teach salad large phrase drip caught coral snake chief mountain manage gym dog alcohol doctor valley evolve citizen'; const CUSTOM_PW = 'above shop hair museum laugh diamond win skin habit inspire hero box category crime boss black nest useless dog alcohol doctor radar urban split'; const CUSTOM_PASSPHRASE = 'correct horse battery staple'; // entropy ffee...00, salt a1b2c3d4e5, internalVersion 1, birthday 0 const BIRTHDAY_ZERO = 'abstract three say vacant about cable tent neck other jeans certain accident arrest market couch front inflict laptop hole marble exact okay owner number'; const split = (phrase: string): string[] => phrase.split(' '); const hex = (b: Uint8Array): string => Buffer.from(b).toString('hex'); describe('isAezeedPhrase', () => { test('accepts aezeed cipher seeds', () => { expect(isAezeedPhrase(split(DEFAULT_PW))).toBe(true); expect(isAezeedPhrase(split(CUSTOM_PW))).toBe(true); expect(isAezeedPhrase(split(BIRTHDAY_ZERO))).toBe(true); }); test('is passphrase-independent — structure only, no scrypt', () => { // Both fixtures hold the SAME entropy under different passphrases. Detection must not care. expect(isAezeedPhrase(split(DEFAULT_PW))).toBe(isAezeedPhrase(split(CUSTOM_PW))); }); test('rejects genuine BIP39 mnemonics', () => { // The whole point: a 24-word BIP39 phrase is also 33 bytes, so only the CRC separates them. for (let i = 0; i < 25; i++) { expect(isAezeedPhrase(split(generateMnemonic(wordlist, 256)))).toBe(false); } }); test('rejects wrong lengths and non-wordlist words', () => { expect(isAezeedPhrase(split(DEFAULT_PW).slice(0, 12))).toBe(false); expect(isAezeedPhrase([...split(DEFAULT_PW).slice(0, 23), 'notaword'])).toBe(false); expect(isAezeedPhrase([])).toBe(false); }); test('rejects a single transposed word', () => { const words = split(DEFAULT_PW); const swapped = [...words]; swapped[3] = words[4]!; swapped[4] = words[3]!; expect(isAezeedPhrase(swapped)).toBe(false); }); }); describe('decipherMnemonic', () => { test('recovers entropy under the default passphrase', async () => { const seed = await decipherMnemonic(split(DEFAULT_PW)); expect(hex(seed.entropy)).toBe(ENTROPY_HEX); expect(seed.entropy).toHaveLength(16); expect(seed.internalVersion).toBe(0); expect(seed.birthday).toBe(5593); }); test('recovers the same entropy under a custom passphrase', async () => { const seed = await decipherMnemonic(split(CUSTOM_PW), CUSTOM_PASSPHRASE); expect(hex(seed.entropy)).toBe(ENTROPY_HEX); }); test('decodes internalVersion 1 and a zero birthday', async () => { const seed = await decipherMnemonic(split(BIRTHDAY_ZERO)); expect(hex(seed.entropy)).toBe('ffeeddccbbaa99887766554433221100'); expect(seed.internalVersion).toBe(1); expect(seed.birthday).toBe(0); }); test('a wrong passphrase fails the AEZ tag rather than returning junk', async () => { // This is the property that makes the whole port safe: a bad key CANNOT yield plausible-looking // entropy, so we can never silently derive the wrong wallet. const err = await decipherMnemonic(split(CUSTOM_PW), 'not the passphrase').catch((e) => e); expect(err).toBeInstanceOf(AezeedError); expect(err.code).toBe('BAD_PASSPHRASE'); }); test('the default passphrase is not silently accepted for a custom-passphrase seed', async () => { const err = await decipherMnemonic(split(CUSTOM_PW)).catch((e) => e); expect(err.code).toBe('BAD_PASSPHRASE'); }); test('a corrupted checksum is caught before scrypt', async () => { const bytes = mnemonicToCipherSeedBytes(split(DEFAULT_PW))!; bytes[32] = bytes[32]! ^ 0xff; const { decipherCipherSeed } = await import('./cipher-seed'); const err = await decipherCipherSeed(bytes, 'aezeed').catch((e) => e); expect(err.code).toBe('BAD_CHECKSUM'); }); test('rejects a non-aezeed phrase', async () => { const err = await decipherMnemonic(split(generateMnemonic(wordlist, 256))).catch((e) => e); expect(err).toBeInstanceOf(AezeedError); }); }); describe('framing', () => { test('24 words pack into exactly 33 bytes', () => { const bytes = mnemonicToCipherSeedBytes(split(DEFAULT_PW)); expect(bytes).toHaveLength(33); expect(bytes![0]).toBe(0); // aezeed version }); test('birthday maps to a real date', () => { // 5593 days after the genesis block. expect(birthdayToDate(5593).toISOString().slice(0, 10)).toBe('2024-04-27'); expect(birthdayToDate(0).toISOString().slice(0, 10)).toBe('2009-01-03'); }); });