diff options
| author | Adam Soltys <[email protected]> | 2026-06-24 15:06:26 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-24 15:06:26 -0700 |
| commit | bdc5779b425151f47a4f5c93566a9f40f952428d (patch) | |
| tree | 0cfc95ac751f0d857d8d4ec2322fe29142c77aa0 /src/wallet.js | |
| parent | c81616ed0024c0cec693b552de5c5b6d7056eff3 (diff) | |
| download | coinosv3-bdc5779b425151f47a4f5c93566a9f40f952428d.tar.gz coinosv3-bdc5779b425151f47a4f5c93566a9f40f952428d.zip | |
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) <[email protected]>
Diffstat (limited to 'src/wallet.js')
| -rw-r--r-- | src/wallet.js | 23 |
1 files changed, 23 insertions, 0 deletions
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)); |
