aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 15:06:26 -0700
committerAdam Soltys <[email protected]>2026-06-24 15:06:26 -0700
commitbdc5779b425151f47a4f5c93566a9f40f952428d (patch)
tree0cfc95ac751f0d857d8d4ec2322fe29142c77aa0 /src/wallet.js
parentc81616ed0024c0cec693b552de5c5b6d7056eff3 (diff)
downloadcoinosv3-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.js23
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));