From bdc5779b425151f47a4f5c93566a9f40f952428d Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 24 Jun 2026 15:06:26 -0700 Subject: Add optional password-encrypted persistence for full accounts Per-account "Save on this device" encrypts a seed account into a localStorage vault (scrypt KDF + XChaCha20-Poly1305 via @noble), so it survives a browser close. On reopen, a vault prompt unlocks all saved wallets with the password (or "Use another wallet" to skip). Default stays session-only; the password is held only in memory for the session and re-encrypts on save/forget. Wrong password fails closed (AEAD). Adds @noble/ciphers dependency. New strings translated across all 19 locales. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wallet.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'src/wallet.js') diff --git a/src/wallet.js b/src/wallet.js index cacd313..4098087 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -13,6 +13,10 @@ import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/b import { wordlist } from '@scure/bip39/wordlists/english'; import { hex, base64urlnopad, base58check } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; +import { scrypt } from '@noble/hashes/scrypt'; +import { randomBytes } from '@noble/hashes/utils'; +import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'; +import { utf8ToBytes, bytesToUtf8 } from '@noble/ciphers/utils.js'; import * as btc from '@scure/btc-signer'; import { p2wpkh } from '@scure/btc-signer/payment'; @@ -1338,6 +1342,25 @@ export function parseAccountKey(s) { return xpub; } +// Encrypted vault for persisting seed-bearing accounts on this device. Key is +// scrypt(password, salt); payload is XChaCha20-Poly1305 (authenticated, so a +// 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 }; +function _vaultKey(password, salt) { + return scrypt(utf8ToBytes(password), salt, _SCRYPT); +} +export function encryptVault(obj, password) { + const 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) }; +} +export function decryptVault(blob, password) { + const pt = xchacha20poly1305(_vaultKey(password, hex.decode(blob.salt)), hex.decode(blob.nonce)).decrypt(hex.decode(blob.ct)); + return JSON.parse(bytesToUtf8(pt)); +} + // Convert a standard account xpub to a BIP84 zpub for export/interop. export function xpubToZpub(xpub) { const data = new Uint8Array(_b58c.decode(xpub)); -- cgit v1.2.3