aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet.js')
-rw-r--r--src/wallet.js38
1 files changed, 33 insertions, 5 deletions
diff --git a/src/wallet.js b/src/wallet.js
index aca11e1..72e0c33 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -9,7 +9,8 @@
// transactions, only the (txid, vout, value, address) of each UTXO.
import { HDKey } from '@scure/bip32';
-import { generateMnemonic, mnemonicToSeedSync, validateMnemonic, entropyToMnemonic } from '@scure/bip39';
+import { generateMnemonic, validateMnemonic, entropyToMnemonic } from '@scure/bip39';
+import { seedSync } from './seed.js';
import { wordlist } from '@scure/bip39/wordlists/english';
import { hex, base64urlnopad, base32nopad, base58check } from '@scure/base';
import { sha256 } from '@noble/hashes/sha256';
@@ -87,7 +88,7 @@ export function accountXpubFor({ mnemonic, passphrase = '', xprv } = {}, netName
const node = HDKey.fromExtendedKey(xprv);
acct = node.depth === 0 ? node.derive(`m/84'/${coin}'/0'`) : node;
} else {
- acct = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)).derive(`m/84'/${coin}'/0'`);
+ acct = HDKey.fromMasterSeed(seedSync(mnemonic, passphrase)).derive(`m/84'/${coin}'/0'`);
}
return acct.publicExtendedKey;
}
@@ -278,7 +279,7 @@ export class Wallet {
const node = HDKey.fromExtendedKey(this.xprv);
acct = node.depth === 0 ? node.derive(`m/84'/${coin}'/${this.accountIndex || 0}'`) : node;
} else {
- acct = HDKey.fromMasterSeed(mnemonicToSeedSync(this.mnemonic, this.passphrase)).derive(`m/84'/${coin}'/${this.accountIndex || 0}'`);
+ acct = HDKey.fromMasterSeed(seedSync(this.mnemonic, this.passphrase)).derive(`m/84'/${coin}'/${this.accountIndex || 0}'`);
}
this._account = acct;
this._accountKey = key;
@@ -2013,11 +2014,38 @@ export function parseExtendedKey(s) {
// wrong password fails to decrypt rather than returning garbage). All fields
// are hex so the blob is JSON-serializable for localStorage.
const _SCRYPT = { N: 2 ** 15, r: 8, p: 1, dkLen: 32 };
+// A vault under the BLANK password (the default: wallets save themselves to
+// the device unless a password is set) gets nothing from scrypt — anyone with
+// the blob can compute scrypt('') — yet paid for it on every cold launch:
+// ~130 ms on a Pixel 9, half a second on a slower phone, before anything
+// opened. Its key is kept beside the blob instead, and new seals reuse that
+// salt so the kept key stays good. A real password is stretched every time,
+// as before, and never kept.
+const BLANK_KEY = 'btc-wallet-vault-k0';
+let _blank = null; // { salt: hex, key: Uint8Array }
+function blankKey() {
+ if (_blank) return _blank;
+ try {
+ const j = JSON.parse(globalThis.localStorage?.getItem(BLANK_KEY) || 'null');
+ if (j && typeof j.salt === 'string' && typeof j.key === 'string') _blank = { salt: j.salt, key: hex.decode(j.key) };
+ } catch {}
+ return _blank;
+}
function _vaultKey(password, salt) {
- return scrypt(utf8ToBytes(password), salt, _SCRYPT);
+ if (password !== '') return scrypt(utf8ToBytes(password), salt, _SCRYPT);
+ const k = blankKey();
+ const sh = hex.encode(salt);
+ if (k && k.salt === sh) return k.key;
+ const key = scrypt(utf8ToBytes(''), salt, _SCRYPT);
+ _blank = { salt: sh, key };
+ try { globalThis.localStorage?.setItem(BLANK_KEY, JSON.stringify({ salt: sh, key: hex.encode(key) })); } catch {}
+ return key;
}
export function encryptVault(obj, password) {
- const salt = randomBytes(16);
+ // sealed under a real password now: the blank one's key has nothing to open
+ if (password !== '') { _blank = null; try { globalThis.localStorage?.removeItem(BLANK_KEY); } catch {} }
+ const kept = password === '' ? blankKey() : null;
+ const salt = kept ? hex.decode(kept.salt) : randomBytes(16);
const nonce = randomBytes(24);
const ct = xchacha20poly1305(_vaultKey(password, salt), nonce).encrypt(utf8ToBytes(JSON.stringify(obj)));
return { v: 1, salt: hex.encode(salt), nonce: hex.encode(nonce), ct: hex.encode(ct) };