aboutsummaryrefslogtreecommitdiffstats
path: root/tools/seed-cache-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/seed-cache-test.js')
-rw-r--r--tools/seed-cache-test.js60
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');