diff options
Diffstat (limited to 'tools/seed-cache-test.js')
| -rw-r--r-- | tools/seed-cache-test.js | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/tools/seed-cache-test.js b/tools/seed-cache-test.js new file mode 100644 index 0000000..6250d65 --- /dev/null +++ b/tools/seed-cache-test.js @@ -0,0 +1,60 @@ +// The seed a phrase stretches to, and the vault under the blank password: +// both now come from caches, and must come out exactly as before. +// seed.js: WebCrypto's PBKDF2 (warmSeed) gives @scure/bip39's seed, byte +// for byte, for plain, passphrased and non-ASCII (NFKD) input; seedSync +// finds it waiting, and works cold. nostr.js's NIP-06 key matches +// nostr-tools' privateKeyFromSeedWords. +// wallet.js: a blank vault's key is kept and reused (no scrypt on the +// second open); a vault sealed under a real password keeps nothing. +// Run: bun tools/seed-cache-test.js +import assert from 'node:assert/strict'; +import { mnemonicToSeedSync, generateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english.js'; +import * as nip06 from 'nostr-tools/nip06'; + +const store = new Map(); +globalThis.localStorage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) }; + +const { seedSync, warmSeed, forgetSeeds } = await import('../src/seed.js'); +const { nostrKeyFromSeed } = await import('../src/nostr.js'); +const { encryptVault, decryptVault } = await import('../src/wallet.js'); +const hex = (u) => Buffer.from(u).toString('hex'); + +const cases = [ + [generateMnemonic(wordlist), ''], + [generateMnemonic(wordlist, 256), 'correct horse'], + [generateMnemonic(wordlist), 'pässwörd Ⅸ'], // NFKD changes these +]; +for (const [m, p] of cases) { + forgetSeeds(); + await warmSeed(m, p); + const t = performance.now(); + const warm = seedSync(m, p); + assert.ok(performance.now() - t < 5, 'a warmed seed is read, not stretched'); + assert.equal(hex(warm), hex(mnemonicToSeedSync(m, p)), 'WebCrypto PBKDF2 = @scure/bip39'); + forgetSeeds(); + assert.equal(hex(seedSync(m, p)), hex(mnemonicToSeedSync(m, p)), 'cold seedSync = @scure/bip39'); + for (const i of [0, 3]) assert.equal(hex(nostrKeyFromSeed(m, p, i)), hex(nip06.privateKeyFromSeedWords(m, p || undefined, i)), 'NIP-06 key = nostr-tools'); +} +await warmSeed('not a phrase', ''); +assert.throws(() => seedSync('not a phrase', ''), 'a malformed phrase is still refused'); +console.log('✓ seeds match @scure/bip39 and nostr-tools, warmed or cold'); + +const list = [{ label: 'a', mnemonic: cases[0][0], passphrase: '' }]; +const blob = encryptVault(list, ''); +assert.ok(store.get('btc-wallet-vault-k0'), 'the blank vault key is kept'); +let t = performance.now(); +assert.deepEqual(decryptVault(blob, ''), list); +assert.ok(performance.now() - t < 30, `opening a blank vault needs no scrypt (${Math.round(performance.now() - t)} ms)`); +const blob2 = encryptVault(list, ''); +assert.equal(blob2.salt, blob.salt, 'a new blank seal reuses the kept salt'); +assert.notEqual(blob2.nonce, blob.nonce, 'never the nonce'); +assert.deepEqual(decryptVault(blob2, ''), list); +assert.throws(() => decryptVault(blob, 'guess'), 'a wrong password still fails'); +assert.ok(store.get('btc-wallet-vault-k0'), 'and a failed guess drops nothing'); +const sealed = encryptVault(list, 'hunter2'); +assert.equal(store.get('btc-wallet-vault-k0'), undefined, 'sealing under a real password keeps nothing'); +assert.deepEqual(decryptVault(sealed, 'hunter2'), list); +assert.throws(() => decryptVault(sealed, '')); +console.log('✓ blank vault key kept and reused; real passwords untouched'); +console.log('\n✅ seed + vault caches behave'); |
