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 | |
| 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]>
| -rwxr-xr-x | bun.lockb | bin | 17437 -> 17469 bytes | |||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/app.js | 161 | ||||
| -rw-r--r-- | src/i18n.js | 17 | ||||
| -rw-r--r-- | src/locales/ar.json | 19 | ||||
| -rw-r--r-- | src/locales/bn.json | 19 | ||||
| -rw-r--r-- | src/locales/de.json | 19 | ||||
| -rw-r--r-- | src/locales/es.json | 19 | ||||
| -rw-r--r-- | src/locales/fr.json | 21 | ||||
| -rw-r--r-- | src/locales/hi.json | 22 | ||||
| -rw-r--r-- | src/locales/id.json | 19 | ||||
| -rw-r--r-- | src/locales/ja.json | 19 | ||||
| -rw-r--r-- | src/locales/ko.json | 22 | ||||
| -rw-r--r-- | src/locales/mr.json | 22 | ||||
| -rw-r--r-- | src/locales/pt.json | 19 | ||||
| -rw-r--r-- | src/locales/ru.json | 19 | ||||
| -rw-r--r-- | src/locales/sw.json | 19 | ||||
| -rw-r--r-- | src/locales/ta.json | 19 | ||||
| -rw-r--r-- | src/locales/te.json | 19 | ||||
| -rw-r--r-- | src/locales/tr.json | 19 | ||||
| -rw-r--r-- | src/locales/ur.json | 19 | ||||
| -rw-r--r-- | src/locales/vi.json | 21 | ||||
| -rw-r--r-- | src/locales/zh.json | 19 | ||||
| -rw-r--r-- | src/wallet.js | 23 |
24 files changed, 538 insertions, 38 deletions
| Binary files differ diff --git a/package.json b/package.json index 48caae1..0ca02a9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "bun run build.js" }, "dependencies": { + "@noble/ciphers": "^2.1.1", "@scure/base": "^1.2.6", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", @@ -4,7 +4,7 @@ // which rebuilds the active screen. Text inputs write back into `ui` on `input` // (without re-rendering) so their values survive structural re-renders. -import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx, giftMinimum, parseAccountKey, xpubToZpub } from './wallet.js'; +import { Wallet, newMnemonic, isValidMnemonic, utxoId, previewGift, buildClaimTx, giftMinimum, parseAccountKey, xpubToZpub, encryptVault, decryptVault } from './wallet.js'; import { qrSvg } from './qr.js'; import { scanQr } from './scan.js'; import { getSyncConfig, setSyncConfig } from './nostr.js'; @@ -30,6 +30,9 @@ const ui = { watchXpub: '', // watch-only xpub/zpub input watchLabel: '', // watch-only account label input fromWallet: false, // unlock screen reached as "add wallet" (show a back button) + pw: null, // { purpose, accId, mode, v1, v2, error } — vault password prompt + vaultPw: '', // on-open vault unlock input + vaultError: '', createStep: 'gen', // 'gen' | 'confirm' draftMnemonic: '', confirm: [], // [{ index, value }] @@ -163,7 +166,9 @@ function render() { ? walletScreen() : ui.screen === 'accounts' ? accountsScreen() - : ui.screen === 'claim' + : ui.screen === 'vault' + ? vaultScreen() + : ui.screen === 'claim' ? claimScreen() : ui.screen === 'howItWorks' ? howItWorksScreen() @@ -677,8 +682,9 @@ function switchAccount(id) { if (acc) activateAccount(acc); } -// Restore accounts after a refresh (sessionStorage), or on a fresh session seed -// the list from persisted watch-only accounts. Returns true if one was opened. +// Restore accounts after a refresh (sessionStorage); on a fresh session, prompt +// to unlock the encrypted vault if there is one, else seed from watch-only +// accounts. Returns true if it handled the entry (opened or showed a prompt). function restoreAccountsState() { let sess = null; try { sess = JSON.parse(sessionStorage.getItem(ACCOUNTS_KEY) || 'null'); } catch {} @@ -688,6 +694,7 @@ function restoreAccountsState() { activateAccount(active); return true; } + if (hasVault()) { ui.screen = 'vault'; render(); return true; } const watch = loadWatchAccounts(); if (watch.length) { accounts = watch.slice(); @@ -697,6 +704,94 @@ function restoreAccountsState() { return false; } +// --- encrypted vault (optional password-persisted full accounts) ---------- +const VAULT_KEY = 'btc-wallet-vault'; +let vaultPassword = null; // in memory once unlocked/set this session; cleared on lock + +function loadVaultBlob() { + try { return JSON.parse(localStorage.getItem(VAULT_KEY) || 'null'); } catch { return null; } +} +function hasVault() { return !!loadVaultBlob(); } + +// Re-encrypt the vault from the currently-persisted full accounts (needs the +// in-memory password). Removes the blob when nothing is persisted. +function writeVault() { + if (vaultPassword == null) return; + const list = accounts.filter((a) => a.type === 'full' && a.persisted) + .map((a) => ({ label: a.label, mnemonic: a.mnemonic, passphrase: a.passphrase || '' })); + try { + if (!list.length) localStorage.removeItem(VAULT_KEY); + else localStorage.setItem(VAULT_KEY, JSON.stringify(encryptVault(list, vaultPassword))); + } catch {} +} + +function mergeVaultList(list) { + for (const v of list) { + const acc = addOrGetAccount({ type: 'full', label: v.label || defaultLabel('full'), mnemonic: v.mnemonic, passphrase: v.passphrase || '' }); + acc.persisted = true; + } +} + +// Toggle persistence on a full account. Prompts for the vault password when it +// isn't unlocked this session ('set' the first time, 'enter' if a vault exists). +function startSave(id) { + const acc = accounts.find((a) => a.id === id); + if (!acc || acc.type !== 'full') return; + if (vaultPassword != null) { acc.persisted = true; writeVault(); persistAccounts(); render(); return; } + ui.pw = { purpose: 'save', accId: id, mode: hasVault() ? 'enter' : 'set', v1: '', v2: '', error: '' }; + render(); +} +function startForget(id) { + if (vaultPassword != null) { + const acc = accounts.find((a) => a.id === id); + if (acc) acc.persisted = false; + writeVault(); persistAccounts(); render(); + return; + } + ui.pw = { purpose: 'forget', accId: id, mode: 'enter', v1: '', v2: '', error: '' }; + render(); +} +function cancelPw() { ui.pw = null; render(); } +function submitPw() { + const p = ui.pw; + if (p.mode === 'set') { + if ((p.v1 || '').length < 8) { p.error = t('pwTooShort'); render(); return; } + if (p.v1 !== p.v2) { p.error = t('pwMismatch'); render(); return; } + vaultPassword = p.v1; + } else { + let list; + try { list = decryptVault(loadVaultBlob(), p.v1); } catch { p.error = t('pwWrong'); render(); return; } + vaultPassword = p.v1; + mergeVaultList(list); // bring existing persisted accounts into the session + } + const acc = accounts.find((a) => a.id === p.accId); + if (acc) acc.persisted = p.purpose === 'save'; + writeVault(); + persistAccounts(); + ui.pw = null; + render(); +} + +// On-open vault unlock. +function unlockVault() { + let list; + try { list = decryptVault(loadVaultBlob(), ui.vaultPw); } catch { ui.vaultError = t('pwWrong'); render(); return; } + vaultPassword = ui.vaultPw; + accounts = list + .map((v) => ({ id: genId(), type: 'full', label: v.label || defaultLabel('full'), mnemonic: v.mnemonic, passphrase: v.passphrase || '', persisted: true })) + .concat(loadWatchAccounts()); + ui.vaultPw = ''; + ui.vaultError = ''; + activateAccount(accounts[0]); +} +function skipVault() { + ui.vaultPw = ''; + ui.vaultError = ''; + const watch = loadWatchAccounts(); + if (watch.length) { accounts = watch.slice(); activateAccount(accounts[0]); } + else { ui.screen = 'unlock'; render(); } +} + function enterOfflineFallback() { wallet.setOffline(true); wallet.deriveWindow(40); @@ -721,13 +816,17 @@ async function retryOnline() { function lock() { wallet.stopRealtime(); clearAccounts(); + vaultPassword = null; wallet.load({ mnemonic: '', passphrase: '', netName: 'mainnet', offline: false }); wallet.mnemonic = ''; - ui.screen = 'unlock'; + ui.screen = hasVault() ? 'vault' : 'unlock'; ui.unlockTab = 'create'; ui.fromWallet = false; ui.watchXpub = ''; ui.watchLabel = ''; + ui.pw = null; + ui.vaultPw = ''; + ui.vaultError = ''; ui.createStep = 'gen'; ui.draftMnemonic = ''; ui.importText = ''; @@ -1179,6 +1278,7 @@ function claimScreen() { // Account switcher: pick a wallet, add another, or lock the session. function accountsScreen() { + if (ui.pw) return h('div', { class: 'col', style: 'gap:16px' }, brandHeader(false), pwPromptCard()); return h( 'div', { class: 'col', style: 'gap:16px' }, @@ -1188,12 +1288,19 @@ function accountsScreen() { h('div', { class: 'col', style: 'gap:0' }, accounts.map((a) => { const isActive = a.id === activeId; - return h('div', { class: 'row between', style: 'padding:10px 0;border-bottom:1px solid var(--line)' }, - h('button', { - class: 'linklike', style: 'text-align:left;flex:1;font-size:15px;' + (isActive ? 'font-weight:600' : ''), - onClick: () => { if (isActive) { ui.screen = 'wallet'; render(); } else switchAccount(a.id); }, - }, (isActive ? '● ' : '○ ') + a.label + (a.type === 'watch' ? ' · ' + t('watchOnlyTag') : '')), - h('button', { class: 'btn-sm', title: t('remove'), onClick: () => removeAccount(a.id) }, '✕') + const tag = a.type === 'watch' ? ' · ' + t('watchOnlyTag') : a.persisted ? ' · ' + t('savedTag') : ''; + return h('div', { class: 'col', style: 'gap:4px; padding:10px 0; border-bottom:1px solid var(--line)' }, + h('div', { class: 'row between' }, + h('button', { + class: 'linklike', style: 'text-align:left;flex:1;font-size:15px;' + (isActive ? 'font-weight:600' : ''), + onClick: () => { if (isActive) { ui.screen = 'wallet'; render(); } else switchAccount(a.id); }, + }, (isActive ? '● ' : '○ ') + a.label + tag), + h('button', { class: 'btn-sm', title: t('remove'), onClick: () => removeAccount(a.id) }, '✕') + ), + a.type === 'full' + ? h('button', { class: 'linklike small', style: 'align-self:flex-start', onClick: () => (a.persisted ? startForget(a.id) : startSave(a.id)) }, + a.persisted ? t('forgetDevice') : t('saveDevice')) + : null ); }) ), @@ -1204,6 +1311,38 @@ function accountsScreen() { ); } +function pwPromptCard() { + const p = ui.pw; + return h('div', { class: 'card col' }, + h('h3', {}, p.mode === 'set' ? t('setPassword') : t('enterPassword')), + h('p', { class: 'small muted', style: 'margin:0' }, p.mode === 'set' ? t('setPasswordDesc') : t('enterPasswordDesc')), + h('input', { type: 'password', placeholder: t('password'), value: p.v1, onInput: (e) => (p.v1 = e.target.value) }), + p.mode === 'set' ? h('input', { type: 'password', placeholder: t('confirmPassword'), value: p.v2, onInput: (e) => (p.v2 = e.target.value) }) : null, + p.error && h('div', { class: 'notice err' }, p.error), + h('div', { class: 'row gap6' }, + h('button', { class: 'btn-ghost grow', onClick: cancelPw }, t('back')), + h('button', { class: 'btn-primary grow', onClick: submitPw }, p.mode === 'set' ? t('save') : t('unlock')) + ) + ); +} + +function vaultScreen() { + return h( + 'div', + { class: 'col', style: 'gap:16px' }, + brandHeader(false), + h('div', { class: 'card col' }, + h('h3', {}, t('unlockSaved')), + h('p', { class: 'small muted', style: 'margin:0' }, t('unlockSavedDesc')), + h('input', { type: 'password', placeholder: t('password'), value: ui.vaultPw, + onInput: (e) => (ui.vaultPw = e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') unlockVault(); } }), + ui.vaultError && h('div', { class: 'notice err' }, ui.vaultError), + h('button', { class: 'btn-primary btn-block', onClick: unlockVault }, t('unlock')), + h('button', { class: 'btn-ghost btn-block', onClick: skipVault }, t('useAnotherWallet')) + ) + ); +} + function walletScreen() { return h( 'div', diff --git a/src/i18n.js b/src/i18n.js index f75e014..9a48796 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -49,6 +49,23 @@ const STR = { labelOptional: 'Label (optional)', walletLabelN: 'Wallet {n}', watchLabelN: 'Watch {n}', + saveDevice: 'Save on this device', + forgetDevice: 'Saved · forget', + savedTag: 'saved', + setPassword: 'Set a password', + setPasswordDesc: 'Encrypts this wallet on this device. You will need it to unlock saved wallets later.', + enterPassword: 'Enter your password', + enterPasswordDesc: 'Unlock your saved wallets to continue.', + password: 'Password', + confirmPassword: 'Confirm password', + pwTooShort: 'Use at least 8 characters.', + pwMismatch: 'Passwords do not match.', + pwWrong: 'Wrong password.', + unlockSaved: 'Unlock saved wallets', + unlockSavedDesc: 'Enter your password to open the wallets saved on this device.', + unlock: 'Unlock', + useAnotherWallet: 'Use another wallet', + save: 'Save', publicKey: 'Public key', publicKeyDesc: 'Share this to watch this wallet read-only on another device or app. It reveals your addresses, not your keys.', copyKey: 'Copy key', diff --git a/src/locales/ar.json b/src/locales/ar.json index cdfb0f8..3522b15 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -189,5 +189,22 @@ "publicKeyDesc": "شارك هذا لمراقبة هذه المحفظة للقراءة فقط على جهاز أو تطبيق آخر. يكشف عناوينك، لا مفاتيحك.", "copyKey": "نسخ المفتاح", "showPublicKey": "إظهار المفتاح العام", - "remove": "إزالة" + "remove": "إزالة", + "saveDevice": "الحفظ على هذا الجهاز", + "forgetDevice": "محفوظة · نسيان", + "savedTag": "محفوظة", + "setPassword": "تعيين كلمة سر", + "setPasswordDesc": "تشفّر هذه المحفظة على هذا الجهاز. ستحتاجها لاحقاً لفتح المحافظ المحفوظة.", + "enterPassword": "أدخل كلمة السر", + "enterPasswordDesc": "افتح محافظك المحفوظة للمتابعة.", + "password": "كلمة السر", + "confirmPassword": "تأكيد كلمة السر", + "pwTooShort": "استخدم 8 أحرف على الأقل.", + "pwMismatch": "كلمتا السر غير متطابقتين.", + "pwWrong": "كلمة سر خاطئة.", + "unlockSaved": "فتح المحافظ المحفوظة", + "unlockSavedDesc": "أدخل كلمة السر لفتح المحافظ المحفوظة على هذا الجهاز.", + "unlock": "فتح", + "useAnotherWallet": "استخدام محفظة أخرى", + "save": "حفظ" }
\ No newline at end of file diff --git a/src/locales/bn.json b/src/locales/bn.json index 211d077..45b2df8 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -187,5 +187,22 @@ "publicKey": "পাবলিক কী", "publicKeyDesc": "অন্য ডিভাইস বা অ্যাপে এই ওয়ালেট শুধু-পঠনযোগ্যভাবে দেখতে এটি শেয়ার করুন। এটি আপনার ঠিকানা প্রকাশ করে, আপনার কী নয়।", "copyKey": "কী কপি", - "showPublicKey": "পাবলিক কী দেখান" + "showPublicKey": "পাবলিক কী দেখান", + "saveDevice": "এই ডিভাইসে সেভ করুন", + "forgetDevice": "সেভ করা · মুছে ফেলুন", + "savedTag": "সেভ করা", + "setPassword": "একটি পাসওয়ার্ড দিন", + "setPasswordDesc": "এই ডিভাইসে এই ওয়ালেট এনক্রিপ্ট করে। পরে সেভ করা ওয়ালেট আনলক করতে আপনার এটি লাগবে।", + "enterPassword": "আপনার পাসওয়ার্ড লিখুন", + "enterPasswordDesc": "চালিয়ে যেতে আপনার সেভ করা ওয়ালেটগুলো আনলক করুন।", + "password": "পাসওয়ার্ড", + "confirmPassword": "পাসওয়ার্ড নিশ্চিত করুন", + "pwTooShort": "অন্তত ৮টি অক্ষর ব্যবহার করুন।", + "pwMismatch": "পাসওয়ার্ড মিলছে না।", + "pwWrong": "ভুল পাসওয়ার্ড।", + "unlockSaved": "সেভ করা ওয়ালেট আনলক করুন", + "unlockSavedDesc": "এই ডিভাইসে সেভ করা ওয়ালেটগুলো খুলতে আপনার পাসওয়ার্ড লিখুন।", + "unlock": "আনলক করুন", + "useAnotherWallet": "অন্য একটি ওয়ালেট ব্যবহার করুন", + "save": "সেভ করুন" }
\ No newline at end of file diff --git a/src/locales/de.json b/src/locales/de.json index fbb88da..515a3f2 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -188,5 +188,22 @@ "publicKey": "Öffentlicher Schlüssel", "publicKeyDesc": "Teile dies, um diese Wallet auf einem anderen Gerät oder in einer anderen App nur-lesend zu beobachten. Er gibt deine Adressen preis, nicht deine Schlüssel.", "copyKey": "Schlüssel kopieren", - "showPublicKey": "Öffentlichen Schlüssel anzeigen" + "showPublicKey": "Öffentlichen Schlüssel anzeigen", + "saveDevice": "Auf diesem Gerät speichern", + "forgetDevice": "Gespeichert · entfernen", + "savedTag": "gespeichert", + "setPassword": "Passwort festlegen", + "setPasswordDesc": "Verschlüsselt diese Wallet auf diesem Gerät. Du brauchst es, um gespeicherte Wallets später zu entsperren.", + "enterPassword": "Passwort eingeben", + "enterPasswordDesc": "Entsperre deine gespeicherten Wallets, um fortzufahren.", + "password": "Passwort", + "confirmPassword": "Passwort bestätigen", + "pwTooShort": "Verwende mindestens 8 Zeichen.", + "pwMismatch": "Die Passwörter stimmen nicht überein.", + "pwWrong": "Falsches Passwort.", + "unlockSaved": "Gespeicherte Wallets entsperren", + "unlockSavedDesc": "Gib dein Passwort ein, um die auf diesem Gerät gespeicherten Wallets zu öffnen.", + "unlock": "Entsperren", + "useAnotherWallet": "Andere Wallet verwenden", + "save": "Speichern" } diff --git a/src/locales/es.json b/src/locales/es.json index a9b8132..09b17f1 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -188,5 +188,22 @@ "giftReclaimed": "Moneda reclamada", "giftRevokeConfirm": "¿Revocar este regalo ahora? Esto devuelve la moneda a tu monedero y desactiva el enlace.", "giftReclaimedTag": "aún reclamable", - "giftMinNote": "Mínimo {n} con las comisiones actuales, para que supere el polvo al reclamarse." + "giftMinNote": "Mínimo {n} con las comisiones actuales, para que supere el polvo al reclamarse.", + "saveDevice": "Guardar en este dispositivo", + "forgetDevice": "Guardado · olvidar", + "savedTag": "guardado", + "setPassword": "Establecer una contraseña", + "setPasswordDesc": "Cifra este monedero en este dispositivo. La necesitarás para desbloquear los monederos guardados más adelante.", + "enterPassword": "Introduce tu contraseña", + "enterPasswordDesc": "Desbloquea tus monederos guardados para continuar.", + "password": "Contraseña", + "confirmPassword": "Confirmar contraseña", + "pwTooShort": "Usa al menos 8 caracteres.", + "pwMismatch": "Las contraseñas no coinciden.", + "pwWrong": "Contraseña incorrecta.", + "unlockSaved": "Desbloquear monederos guardados", + "unlockSavedDesc": "Introduce tu contraseña para abrir los monederos guardados en este dispositivo.", + "unlock": "Desbloquear", + "useAnotherWallet": "Usar otro monedero", + "save": "Guardar" }
\ No newline at end of file diff --git a/src/locales/fr.json b/src/locales/fr.json index b09cd92..6aa57be 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -188,5 +188,22 @@ "publicKey": "Clé publique", "publicKeyDesc": "Partagez-la pour surveiller ce portefeuille en lecture seule sur un autre appareil ou une autre application. Elle révèle vos adresses, pas vos clés.", "copyKey": "Copier la clé", - "showPublicKey": "Afficher la clé publique" -}
\ No newline at end of file + "showPublicKey": "Afficher la clé publique", + "saveDevice": "Enregistrer sur cet appareil", + "forgetDevice": "Enregistré · oublier", + "savedTag": "enregistré", + "setPassword": "Définir un mot de passe", + "setPasswordDesc": "Chiffre ce portefeuille sur cet appareil. Vous en aurez besoin pour déverrouiller les portefeuilles enregistrés plus tard.", + "enterPassword": "Saisissez votre mot de passe", + "enterPasswordDesc": "Déverrouillez vos portefeuilles enregistrés pour continuer.", + "password": "Mot de passe", + "confirmPassword": "Confirmer le mot de passe", + "pwTooShort": "Utilisez au moins 8 caractères.", + "pwMismatch": "Les mots de passe ne correspondent pas.", + "pwWrong": "Mot de passe incorrect.", + "unlockSaved": "Déverrouiller les portefeuilles enregistrés", + "unlockSavedDesc": "Saisissez votre mot de passe pour ouvrir les portefeuilles enregistrés sur cet appareil.", + "unlock": "Déverrouiller", + "useAnotherWallet": "Utiliser un autre portefeuille", + "save": "Enregistrer" +} diff --git a/src/locales/hi.json b/src/locales/hi.json index b3cbc36..c7418cb 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -176,7 +176,6 @@ "accounts": "खाते", "addWallet": "वॉलेट जोड़ें", "lockAll": "सभी लॉक करें", - "remove": "हटाएँ", "watchOnly": "केवल-देखें", "watchOnlyTag": "केवल-देखें", "watchOnlyAdd": "केवल-देखें जोड़ें", @@ -189,5 +188,22 @@ "publicKey": "पब्लिक की", "publicKeyDesc": "इस वॉलेट को किसी अन्य डिवाइस या ऐप पर केवल-देखें मोड में देखने के लिए इसे साझा करें। यह आपके पते दिखाता है, आपकी कुंजियाँ नहीं।", "copyKey": "की कॉपी करें", - "showPublicKey": "पब्लिक की दिखाएँ" -}
\ No newline at end of file + "showPublicKey": "पब्लिक की दिखाएँ", + "saveDevice": "इस डिवाइस पर सहेजें", + "forgetDevice": "सहेजा गया · भूल जाएँ", + "savedTag": "सहेजा गया", + "setPassword": "पासवर्ड सेट करें", + "setPasswordDesc": "इस डिवाइस पर इस वॉलेट को एन्क्रिप्ट करता है। बाद में सहेजे गए वॉलेट अनलॉक करने के लिए आपको इसकी ज़रूरत होगी।", + "enterPassword": "अपना पासवर्ड दर्ज करें", + "enterPasswordDesc": "जारी रखने के लिए अपने सहेजे गए वॉलेट अनलॉक करें।", + "password": "पासवर्ड", + "confirmPassword": "पासवर्ड की पुष्टि करें", + "pwTooShort": "कम से कम 8 अक्षर का उपयोग करें।", + "pwMismatch": "पासवर्ड मेल नहीं खाते।", + "pwWrong": "गलत पासवर्ड।", + "unlockSaved": "सहेजे गए वॉलेट अनलॉक करें", + "unlockSavedDesc": "इस डिवाइस पर सहेजे गए वॉलेट खोलने के लिए अपना पासवर्ड दर्ज करें।", + "unlock": "अनलॉक करें", + "useAnotherWallet": "दूसरा वॉलेट उपयोग करें", + "save": "सहेजें" +} diff --git a/src/locales/id.json b/src/locales/id.json index 9d6befc..d5d9bbc 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -188,5 +188,22 @@ "publicKey": "Kunci publik", "publicKeyDesc": "Bagikan ini untuk memantau dompet ini secara baca-saja di perangkat atau aplikasi lain. Ini menampilkan alamat Anda, bukan kunci Anda.", "copyKey": "Salin kunci", - "showPublicKey": "Tampilkan kunci publik" + "showPublicKey": "Tampilkan kunci publik", + "saveDevice": "Simpan di perangkat ini", + "forgetDevice": "Tersimpan · lupakan", + "savedTag": "tersimpan", + "setPassword": "Atur kata sandi", + "setPasswordDesc": "Mengenkripsi dompet ini di perangkat ini. Anda akan membutuhkannya untuk membuka dompet tersimpan nanti.", + "enterPassword": "Masukkan kata sandi Anda", + "enterPasswordDesc": "Buka dompet tersimpan Anda untuk melanjutkan.", + "password": "Kata sandi", + "confirmPassword": "Konfirmasi kata sandi", + "pwTooShort": "Gunakan minimal 8 karakter.", + "pwMismatch": "Kata sandi tidak cocok.", + "pwWrong": "Kata sandi salah.", + "unlockSaved": "Buka dompet tersimpan", + "unlockSavedDesc": "Masukkan kata sandi Anda untuk membuka dompet yang tersimpan di perangkat ini.", + "unlock": "Buka", + "useAnotherWallet": "Gunakan dompet lain", + "save": "Simpan" }
\ No newline at end of file diff --git a/src/locales/ja.json b/src/locales/ja.json index b990fd8..55ea846 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -188,5 +188,22 @@ "publicKey": "公開鍵", "publicKeyDesc": "これを共有すると、別の端末やアプリでこのウォレットを読み取り専用でウォッチできます。鍵ではなくアドレスが公開されます。", "copyKey": "鍵をコピー", - "showPublicKey": "公開鍵を表示" + "showPublicKey": "公開鍵を表示", + "saveDevice": "この端末に保存", + "forgetDevice": "保存済み · 削除", + "savedTag": "保存済み", + "setPassword": "パスワードを設定", + "setPasswordDesc": "このウォレットをこの端末で暗号化します。後で保存したウォレットを解除するために必要です。", + "enterPassword": "パスワードを入力", + "enterPasswordDesc": "保存したウォレットを解除して続行します。", + "password": "パスワード", + "confirmPassword": "パスワードを再入力", + "pwTooShort": "8文字以上にしてください。", + "pwMismatch": "パスワードが一致しません。", + "pwWrong": "パスワードが違います。", + "unlockSaved": "保存したウォレットを解除", + "unlockSavedDesc": "この端末に保存したウォレットを開くにはパスワードを入力してください。", + "unlock": "解除", + "useAnotherWallet": "別のウォレットを使う", + "save": "保存" } diff --git a/src/locales/ko.json b/src/locales/ko.json index 5227148..dbe6775 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -176,7 +176,6 @@ "accounts": "계정", "addWallet": "지갑 추가", "lockAll": "모두 잠그기", - "remove": "삭제", "watchOnly": "조회 전용", "watchOnlyTag": "조회 전용", "watchOnlyAdd": "조회 전용 추가", @@ -189,5 +188,22 @@ "publicKey": "공개키", "publicKeyDesc": "이 키를 공유하면 다른 기기나 앱에서 이 지갑을 조회 전용으로 볼 수 있습니다. 주소는 드러나지만 키는 드러나지 않습니다.", "copyKey": "키 복사", - "showPublicKey": "공개키 표시" -}
\ No newline at end of file + "showPublicKey": "공개키 표시", + "saveDevice": "이 기기에 저장", + "forgetDevice": "저장됨 · 삭제", + "savedTag": "저장됨", + "setPassword": "비밀번호 설정", + "setPasswordDesc": "이 지갑을 이 기기에서 암호화합니다. 나중에 저장된 지갑을 잠금 해제하려면 이 비밀번호가 필요합니다.", + "enterPassword": "비밀번호를 입력하세요", + "enterPasswordDesc": "계속하려면 저장된 지갑을 잠금 해제하세요.", + "password": "비밀번호", + "confirmPassword": "비밀번호 확인", + "pwTooShort": "8자 이상 사용하세요.", + "pwMismatch": "비밀번호가 일치하지 않습니다.", + "pwWrong": "비밀번호가 틀렸습니다.", + "unlockSaved": "저장된 지갑 잠금 해제", + "unlockSavedDesc": "이 기기에 저장된 지갑을 열려면 비밀번호를 입력하세요.", + "unlock": "잠금 해제", + "useAnotherWallet": "다른 지갑 사용", + "save": "저장" +} diff --git a/src/locales/mr.json b/src/locales/mr.json index ed90448..2481153 100644 --- a/src/locales/mr.json +++ b/src/locales/mr.json @@ -176,7 +176,6 @@ "accounts": "खाती", "addWallet": "वॉलेट जोडा", "lockAll": "सर्व लॉक करा", - "remove": "काढा", "watchOnly": "फक्त-पाहण्यासाठी", "watchOnlyTag": "फक्त-पाहण्यासाठी", "watchOnlyAdd": "फक्त-पाहण्यासाठी जोडा", @@ -189,5 +188,22 @@ "publicKey": "सार्वजनिक की", "publicKeyDesc": "दुसऱ्या उपकरणावर किंवा अॅपवर हे वॉलेट फक्त-वाचण्यासाठी पाहण्यासाठी हे शेअर करा. यातून तुमचे पत्ते दिसतात, तुमच्या की नाहीत.", "copyKey": "की कॉपी करा", - "showPublicKey": "सार्वजनिक की दाखवा" -}
\ No newline at end of file + "showPublicKey": "सार्वजनिक की दाखवा", + "saveDevice": "या डिव्हाइसवर सेव्ह करा", + "forgetDevice": "सेव्ह केले · विसरा", + "savedTag": "सेव्ह केले", + "setPassword": "पासवर्ड सेट करा", + "setPasswordDesc": "हे वॉलेट या डिव्हाइसवर एन्क्रिप्ट करते. सेव्ह केलेली वॉलेट नंतर अनलॉक करण्यासाठी तुम्हाला त्याची गरज लागेल.", + "enterPassword": "तुमचा पासवर्ड टाका", + "enterPasswordDesc": "पुढे जाण्यासाठी तुमची सेव्ह केलेली वॉलेट अनलॉक करा.", + "password": "पासवर्ड", + "confirmPassword": "पासवर्डची खात्री करा", + "pwTooShort": "किमान ८ अक्षरे वापरा.", + "pwMismatch": "पासवर्ड जुळत नाहीत.", + "pwWrong": "चुकीचा पासवर्ड.", + "unlockSaved": "सेव्ह केलेली वॉलेट अनलॉक करा", + "unlockSavedDesc": "या डिव्हाइसवर सेव्ह केलेली वॉलेट उघडण्यासाठी तुमचा पासवर्ड टाका.", + "unlock": "अनलॉक करा", + "useAnotherWallet": "दुसरे वॉलेट वापरा", + "save": "सेव्ह करा" +} diff --git a/src/locales/pt.json b/src/locales/pt.json index 9e9d210..fdb7828 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -189,5 +189,22 @@ "publicKey": "Chave pública", "publicKeyDesc": "Compartilhe isto para acompanhar esta carteira somente leitura em outro dispositivo ou aplicativo. Revela seus endereços, não suas chaves.", "copyKey": "Copiar chave", - "showPublicKey": "Mostrar chave pública" + "showPublicKey": "Mostrar chave pública", + "saveDevice": "Salvar neste dispositivo", + "forgetDevice": "Salva · esquecer", + "savedTag": "salva", + "setPassword": "Definir uma senha", + "setPasswordDesc": "Criptografa esta carteira neste dispositivo. Você precisará dela para desbloquear as carteiras salvas depois.", + "enterPassword": "Digite sua senha", + "enterPasswordDesc": "Desbloqueie suas carteiras salvas para continuar.", + "password": "Senha", + "confirmPassword": "Confirmar senha", + "pwTooShort": "Use ao menos 8 caracteres.", + "pwMismatch": "As senhas não coincidem.", + "pwWrong": "Senha incorreta.", + "unlockSaved": "Desbloquear carteiras salvas", + "unlockSavedDesc": "Digite sua senha para abrir as carteiras salvas neste dispositivo.", + "unlock": "Desbloquear", + "useAnotherWallet": "Usar outra carteira", + "save": "Salvar" }
\ No newline at end of file diff --git a/src/locales/ru.json b/src/locales/ru.json index 07112ce..1649fd6 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -189,5 +189,22 @@ "publicKey": "Публичный ключ", "publicKeyDesc": "Поделитесь этим, чтобы следить за кошельком только для чтения на другом устройстве или в приложении. Это раскрывает ваши адреса, но не ключи.", "copyKey": "Копировать ключ", - "showPublicKey": "Показать публичный ключ" + "showPublicKey": "Показать публичный ключ", + "saveDevice": "Сохранить на этом устройстве", + "forgetDevice": "Сохранено · забыть", + "savedTag": "сохранено", + "setPassword": "Задать пароль", + "setPasswordDesc": "Шифрует этот кошелёк на этом устройстве. Он понадобится, чтобы позже разблокировать сохранённые кошельки.", + "enterPassword": "Введите пароль", + "enterPasswordDesc": "Разблокируйте сохранённые кошельки, чтобы продолжить.", + "password": "Пароль", + "confirmPassword": "Подтвердите пароль", + "pwTooShort": "Используйте не менее 8 символов.", + "pwMismatch": "Пароли не совпадают.", + "pwWrong": "Неверный пароль.", + "unlockSaved": "Разблокировать сохранённые кошельки", + "unlockSavedDesc": "Введите пароль, чтобы открыть кошельки, сохранённые на этом устройстве.", + "unlock": "Разблокировать", + "useAnotherWallet": "Использовать другой кошелёк", + "save": "Сохранить" }
\ No newline at end of file diff --git a/src/locales/sw.json b/src/locales/sw.json index 98a53a4..5876383 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -189,5 +189,22 @@ "publicKey": "Ufunguo wa umma", "publicKeyDesc": "Shiriki huu ili kutazama pochi hii kwa kusoma tu kwenye kifaa au programu nyingine. Unaonyesha anwani zako, si funguo zako.", "copyKey": "Nakili ufunguo", - "showPublicKey": "Onyesha ufunguo wa umma" + "showPublicKey": "Onyesha ufunguo wa umma", + "saveDevice": "Hifadhi kwenye kifaa hiki", + "forgetDevice": "Imehifadhiwa · sahau", + "savedTag": "imehifadhiwa", + "setPassword": "Weka nenosiri", + "setPasswordDesc": "Husimba pochi hii kwa njia fiche kwenye kifaa hiki. Utalihitaji ili kufungua pochi zilizohifadhiwa baadaye.", + "enterPassword": "Ingiza nenosiri lako", + "enterPasswordDesc": "Fungua pochi zako zilizohifadhiwa ili kuendelea.", + "password": "Nenosiri", + "confirmPassword": "Thibitisha nenosiri", + "pwTooShort": "Tumia angalau herufi 8.", + "pwMismatch": "Manenosiri hayalingani.", + "pwWrong": "Nenosiri si sahihi.", + "unlockSaved": "Fungua pochi zilizohifadhiwa", + "unlockSavedDesc": "Ingiza nenosiri lako ili kufungua pochi zilizohifadhiwa kwenye kifaa hiki.", + "unlock": "Fungua", + "useAnotherWallet": "Tumia pochi nyingine", + "save": "Hifadhi" }
\ No newline at end of file diff --git a/src/locales/ta.json b/src/locales/ta.json index b157a31..f772d43 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -189,5 +189,22 @@ "publicKey": "பொது விசை", "publicKeyDesc": "மற்றொரு சாதனத்தில் அல்லது செயலியில் இந்த வாலட்டைப் பார்வை மட்டும் கண்காணிக்க இதைப் பகிரவும். இது உங்கள் முகவரிகளை வெளிப்படுத்தும், உங்கள் விசைகளை அல்ல.", "copyKey": "விசையை நகலெடு", - "showPublicKey": "பொது விசையைக் காட்டு" + "showPublicKey": "பொது விசையைக் காட்டு", + "saveDevice": "இந்தச் சாதனத்தில் சேமி", + "forgetDevice": "சேமிக்கப்பட்டது · மறக்கடி", + "savedTag": "சேமிக்கப்பட்டது", + "setPassword": "கடவுச்சொல்லை அமை", + "setPasswordDesc": "இந்தச் சாதனத்தில் இந்த வாலட்டை மறையாக்குகிறது. சேமித்த வாலட்டுகளைப் பின்னர் திறக்க இது தேவைப்படும்.", + "enterPassword": "உங்கள் கடவுச்சொல்லை உள்ளிடவும்", + "enterPasswordDesc": "தொடர சேமித்த வாலட்டுகளைத் திறக்கவும்.", + "password": "கடவுச்சொல்", + "confirmPassword": "கடவுச்சொல்லை உறுதிப்படுத்து", + "pwTooShort": "குறைந்தது 8 எழுத்துகளைப் பயன்படுத்தவும்.", + "pwMismatch": "கடவுச்சொற்கள் பொருந்தவில்லை.", + "pwWrong": "தவறான கடவுச்சொல்.", + "unlockSaved": "சேமித்த வாலட்டுகளைத் திற", + "unlockSavedDesc": "இந்தச் சாதனத்தில் சேமித்த வாலட்டுகளைத் திறக்க உங்கள் கடவுச்சொல்லை உள்ளிடவும்.", + "unlock": "திற", + "useAnotherWallet": "வேறு வாலட்டைப் பயன்படுத்து", + "save": "சேமி" }
\ No newline at end of file diff --git a/src/locales/te.json b/src/locales/te.json index 37380ff..babcb33 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -187,5 +187,22 @@ "publicKey": "పబ్లిక్ కీ", "publicKeyDesc": "ఈ వాలెట్ను మరో పరికరం లేదా యాప్లో చదవడం-మాత్రమే గమనించడానికి దీన్ని పంచుకోండి. ఇది మీ చిరునామాలను వెల్లడిస్తుంది, మీ కీలను కాదు.", "copyKey": "కీ కాపీ", - "showPublicKey": "పబ్లిక్ కీ చూపు" + "showPublicKey": "పబ్లిక్ కీ చూపు", + "saveDevice": "ఈ పరికరంలో సేవ్ చేయి", + "forgetDevice": "సేవ్ చేయబడింది · మర్చిపో", + "savedTag": "సేవ్ చేయబడింది", + "setPassword": "పాస్వర్డ్ సెట్ చేయండి", + "setPasswordDesc": "ఈ వాలెట్ను ఈ పరికరంలో ఎన్క్రిప్ట్ చేస్తుంది. సేవ్ చేసిన వాలెట్లను తర్వాత అన్లాక్ చేయడానికి ఇది అవసరం.", + "enterPassword": "మీ పాస్వర్డ్ నమోదు చేయండి", + "enterPasswordDesc": "కొనసాగడానికి మీ సేవ్ చేసిన వాలెట్లను అన్లాక్ చేయండి.", + "password": "పాస్వర్డ్", + "confirmPassword": "పాస్వర్డ్ నిర్ధారించండి", + "pwTooShort": "కనీసం 8 అక్షరాలు వాడండి.", + "pwMismatch": "పాస్వర్డ్లు సరిపోలడం లేదు.", + "pwWrong": "తప్పు పాస్వర్డ్.", + "unlockSaved": "సేవ్ చేసిన వాలెట్లను అన్లాక్ చేయి", + "unlockSavedDesc": "ఈ పరికరంలో సేవ్ చేసిన వాలెట్లను తెరవడానికి మీ పాస్వర్డ్ నమోదు చేయండి.", + "unlock": "అన్లాక్", + "useAnotherWallet": "మరో వాలెట్ వాడండి", + "save": "సేవ్ చేయి" }
\ No newline at end of file diff --git a/src/locales/tr.json b/src/locales/tr.json index 0277452..50acb8c 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -188,5 +188,22 @@ "publicKey": "Genel anahtar", "publicKeyDesc": "Bu cüzdanı başka bir cihaz veya uygulamada salt okunur izlemek için bunu paylaşın. Anahtarlarınızı değil, adreslerinizi açığa çıkarır.", "copyKey": "Anahtarı kopyala", - "showPublicKey": "Genel anahtarı göster" + "showPublicKey": "Genel anahtarı göster", + "saveDevice": "Bu cihaza kaydet", + "forgetDevice": "Kaydedildi · unut", + "savedTag": "kaydedildi", + "setPassword": "Parola belirleyin", + "setPasswordDesc": "Bu cüzdanı bu cihazda şifreler. Kaydedilen cüzdanları daha sonra açmak için bu parolaya ihtiyacınız olacak.", + "enterPassword": "Parolanızı girin", + "enterPasswordDesc": "Devam etmek için kaydedilen cüzdanlarınızı açın.", + "password": "Parola", + "confirmPassword": "Parolayı onayla", + "pwTooShort": "En az 8 karakter kullanın.", + "pwMismatch": "Parolalar eşleşmiyor.", + "pwWrong": "Yanlış parola.", + "unlockSaved": "Kaydedilen cüzdanları aç", + "unlockSavedDesc": "Bu cihaza kaydedilen cüzdanları açmak için parolanızı girin.", + "unlock": "Aç", + "useAnotherWallet": "Başka bir cüzdan kullan", + "save": "Kaydet" }
\ No newline at end of file diff --git a/src/locales/ur.json b/src/locales/ur.json index a8c2e56..55f7801 100644 --- a/src/locales/ur.json +++ b/src/locales/ur.json @@ -189,5 +189,22 @@ "publicKey": "پبلک کلید", "publicKeyDesc": "اسے شیئر کریں تاکہ کسی اور ڈیوائس یا ایپ پر اس والیٹ کو صرف پڑھنے کے لیے دیکھا جا سکے۔ یہ آپ کے پتے ظاہر کرتی ہے، کلیدیں نہیں۔", "copyKey": "کلید کاپی کریں", - "showPublicKey": "پبلک کلید دکھائیں" + "showPublicKey": "پبلک کلید دکھائیں", + "saveDevice": "اس ڈیوائس پر محفوظ کریں", + "forgetDevice": "محفوظ شدہ · بھول جائیں", + "savedTag": "محفوظ شدہ", + "setPassword": "پاس ورڈ مقرر کریں", + "setPasswordDesc": "اس ڈیوائس پر اس والیٹ کو خفیہ کرتا ہے۔ بعد میں محفوظ شدہ والیٹ کھولنے کے لیے آپ کو یہ درکار ہوگا۔", + "enterPassword": "اپنا پاس ورڈ درج کریں", + "enterPasswordDesc": "جاری رکھنے کے لیے اپنے محفوظ شدہ والیٹ کھولیں۔", + "password": "پاس ورڈ", + "confirmPassword": "پاس ورڈ کی تصدیق کریں", + "pwTooShort": "کم از کم 8 حروف استعمال کریں۔", + "pwMismatch": "پاس ورڈ میل نہیں کھاتے۔", + "pwWrong": "غلط پاس ورڈ۔", + "unlockSaved": "محفوظ شدہ والیٹ کھولیں", + "unlockSavedDesc": "اس ڈیوائس پر محفوظ شدہ والیٹ کھولنے کے لیے اپنا پاس ورڈ درج کریں۔", + "unlock": "کھولیں", + "useAnotherWallet": "کوئی اور والیٹ استعمال کریں", + "save": "محفوظ کریں" }
\ No newline at end of file diff --git a/src/locales/vi.json b/src/locales/vi.json index 4c8e58c..0c319f1 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -188,5 +188,22 @@ "publicKey": "Khóa công khai", "publicKeyDesc": "Chia sẻ khóa này để theo dõi ví này ở chế độ chỉ đọc trên thiết bị hoặc ứng dụng khác. Nó tiết lộ địa chỉ của bạn, chứ không phải khóa của bạn.", "copyKey": "Sao chép khóa", - "showPublicKey": "Hiện khóa công khai" -}
\ No newline at end of file + "showPublicKey": "Hiện khóa công khai", + "saveDevice": "Lưu trên thiết bị này", + "forgetDevice": "Đã lưu · xóa", + "savedTag": "đã lưu", + "setPassword": "Đặt mật khẩu", + "setPasswordDesc": "Mã hóa ví này trên thiết bị này. Bạn sẽ cần nó để mở khóa các ví đã lưu sau này.", + "enterPassword": "Nhập mật khẩu của bạn", + "enterPasswordDesc": "Mở khóa các ví đã lưu để tiếp tục.", + "password": "Mật khẩu", + "confirmPassword": "Xác nhận mật khẩu", + "pwTooShort": "Dùng ít nhất 8 ký tự.", + "pwMismatch": "Mật khẩu không khớp.", + "pwWrong": "Sai mật khẩu.", + "unlockSaved": "Mở khóa ví đã lưu", + "unlockSavedDesc": "Nhập mật khẩu để mở các ví đã lưu trên thiết bị này.", + "unlock": "Mở khóa", + "useAnotherWallet": "Dùng ví khác", + "save": "Lưu" +} diff --git a/src/locales/zh.json b/src/locales/zh.json index cf7de6f..72bc340 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -189,5 +189,22 @@ "publicKey": "公钥", "publicKeyDesc": "分享它即可在另一台设备或应用上以只读方式查看此钱包。它会暴露你的地址,而非你的密钥。", "copyKey": "复制密钥", - "showPublicKey": "显示公钥" + "showPublicKey": "显示公钥", + "saveDevice": "保存到此设备", + "forgetDevice": "已保存 · 忘记", + "savedTag": "已保存", + "setPassword": "设置密码", + "setPasswordDesc": "在此设备上加密此钱包。日后需用它解锁已保存的钱包。", + "enterPassword": "输入你的密码", + "enterPasswordDesc": "解锁已保存的钱包以继续。", + "password": "密码", + "confirmPassword": "确认密码", + "pwTooShort": "请至少使用 8 个字符。", + "pwMismatch": "两次密码不一致。", + "pwWrong": "密码错误。", + "unlockSaved": "解锁已保存的钱包", + "unlockSavedDesc": "输入你的密码以打开此设备上保存的钱包。", + "unlock": "解锁", + "useAnotherWallet": "使用其他钱包", + "save": "保存" }
\ No newline at end of file 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)); |
