aboutsummaryrefslogtreecommitdiffstats
path: root/src/nostr.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-06 22:55:11 -0700
committerAdam Soltys <[email protected]>2026-11-06 22:55:11 -0700
commit1fe10057f301b09bbad0cfccdfecac9826733048 (patch)
tree183ec6f7eabd5de3b4dd1af53e692f6be8264563 /src/nostr.js
parentc44377f0f103c2f17805d9e0bf4d2688e962af5e (diff)
downloadcoinosv3-1fe10057f301b09bbad0cfccdfecac9826733048.tar.gz
coinosv3-1fe10057f301b09bbad0cfccdfecac9826733048.zip
Opening the wallet no longer stretches the same keys in JavaScript two and three times over. Profiled on Adam's Pixel 9 at 4x CPU (a mid-range phone), a cold launch — a fresh tab, so the wallet comes out of the device vault — spent ~850 ms of its 1.9 s to the balance stretching keys on the main thread: scrypt (N=2^15) to open the vault under its blank password (~520 ms), then PBKDF2-2048 of the same twelve words once per saved wallet to match it to its watch-only entry and twice more for the open one (the BIP84 account and, inside nostr-tools' nip06, the nostr identity). Now src/seed.js keeps each phrase's BIP-39 seed: the boot stretches the phrases it's about to open with WebCrypto's native PBKDF2 while the stores open (warmBootSeeds, alongside the IndexedDB wait it already had), and the wallet, zaps and the nostr key (NIP-06 m/44'/1237'/n'/0/0, derived from that seed — nostrKeyFromSeed) read it; a reader that comes first stretches it the old way and leaves it for the rest. A lock forgets them. The vault under the BLANK password — the default, which scrypt protects from no one, since anyone with the blob can compute scrypt('') — keeps its key beside the blob and new blank seals reuse that salt (fresh nonce each time); sealing under a real password drops it, and real passwords are stretched every time as before. Same cold launch at 4x: balance 2.2–2.9 s → ~1.5 s, scrypt and the JS PBKDF2 gone from the profile. tools/seed-cache-test.js: WebCrypto's seed = @scure/bip39's for plain, passphrased and NFKD input, warmed or cold; the NIP-06 key = nostr-tools'; a malformed phrase still refused; a blank vault opens without scrypt and reseals with the kept salt, a wrong guess fails and drops nothing, a real password keeps nothing
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6Ts5Tktega8VbLg31q
Diffstat (limited to 'src/nostr.js')
-rw-r--r--src/nostr.js19
1 files changed, 14 insertions, 5 deletions
diff --git a/src/nostr.js b/src/nostr.js
index 731c076..4b7f5c3 100644
--- a/src/nostr.js
+++ b/src/nostr.js
@@ -9,7 +9,8 @@
// Sync is on by default (relay.coinos.io) but can be turned off or pointed at
// other relays in Settings; the preference lives in localStorage.
-import * as nip06 from 'nostr-tools/nip06';
+import { HDKey } from '@scure/bip32';
+import { seedSync } from './seed.js';
import * as nip44 from 'nostr-tools/nip44';
import * as nip04 from 'nostr-tools/nip04';
import { getPublicKey, finalizeEvent, generateSecretKey, verifyEvent } from 'nostr-tools/pure';
@@ -650,13 +651,21 @@ export { getPublicKey, finalizeEvent, generateSecretKey };
// The nostr identity a seed carries (NostrSync#load's key, without the
// wallet) — so the account switcher can put a face on every account on the
-// device, not just the one that's open. Memoized: nip06 stretches the seed.
+// device, not just the one that's open. Memoized: the seed is stretched.
+// NIP-06: m/44'/1237'/<account>'/0/0 of the BIP-39 seed — what nostr-tools'
+// privateKeyFromSeedWords derives, from the seed the wallet already stretched
+// (seed.js) instead of stretching the words a second time.
+export function nostrKeyFromSeed(mnemonic, passphrase = '', accountIndex = 0) {
+ const sk = HDKey.fromMasterSeed(seedSync(mnemonic, passphrase || '')).derive(`m/44'/1237'/${accountIndex || 0}'/0/0`).privateKey;
+ if (!sk) throw new Error('could not derive private key');
+ return sk;
+}
const _seedPks = new Map();
export function seedPubkey(mnemonic, passphrase = '', accountIndex = 0) {
const k = `${mnemonic}\n${passphrase}\n${accountIndex}`;
if (_seedPks.has(k)) return _seedPks.get(k);
let pk = null;
- try { pk = getPublicKey(nip06.privateKeyFromSeedWords(mnemonic, passphrase || undefined, accountIndex || 0)); } catch {}
+ try { pk = getPublicKey(nostrKeyFromSeed(mnemonic, passphrase, accountIndex)); } catch {}
_seedPks.set(k, pk);
return pk;
}
@@ -839,8 +848,8 @@ export class NostrSync {
load(mnemonic, passphrase = '', accountIndex = 0) {
// a derived sibling wallet (same seed, higher BIP84 account) gets its own
- // nostr identity and sync slots — nip06 shifts by account index
- this.sk = nip06.privateKeyFromSeedWords(mnemonic, passphrase || undefined, accountIndex || 0);
+ // nostr identity and sync slots — NIP-06 shifts by account index
+ this.sk = nostrKeyFromSeed(mnemonic, passphrase, accountIndex);
this.pk = getPublicKey(this.sk);
this.ck = nip44.getConversationKey(this.sk, this.pk);
}