aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-26 12:01:04 -0700
committerAdam Soltys <[email protected]>2026-06-26 12:01:04 -0700
commit8b8e5a7e27fdb491a11771f9b46f159c87e7d3e3 (patch)
tree773dbe820c1335a76ed5996f85eb8051a185eb58 /src
parent634ed2c91f64e478e6dfff676d402eb15d97af21 (diff)
downloadcoinosv3-8b8e5a7e27fdb491a11771f9b46f159c87e7d3e3.tar.gz
coinosv3-8b8e5a7e27fdb491a11771f9b46f159c87e7d3e3.zip
Add 'Change password' for the saved-to-device vault
A vault exists → Accounts screen shows Change password: enter the current password, then a new one (with confirm; leave blank to remove it). It decrypts the actual vault and re-encrypts it with the new password, so wallets not loaded in the current session are never dropped. Keeps the in-memory password in sync when the vault is already unlocked. Verified the re-encrypt round-trip (old rejected, new works, blank works). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js31
-rw-r--r--src/i18n.js4
2 files changed, 30 insertions, 5 deletions
diff --git a/src/app.js b/src/app.js
index 87bfb86..333b0ef 100644
--- a/src/app.js
+++ b/src/app.js
@@ -915,8 +915,25 @@ function startForget(id) {
render();
}
function cancelPw() { ui.pw = null; render(); }
+function startChangePw() {
+ ui.pw = { purpose: 'change', mode: 'change', v0: '', v1: '', v2: '', error: '' };
+ render();
+}
function submitPw() {
const p = ui.pw;
+ if (p.purpose === 'change') {
+ // Re-encrypt the actual vault (decrypt with the current password, encrypt
+ // with the new one) so we never drop wallets that aren't in this session.
+ let list;
+ try { list = decryptVault(loadVaultBlob(), p.v0 || ''); } catch { p.error = t('pwWrong'); render(); return; }
+ if (p.v1 !== p.v2) { p.error = t('pwMismatch'); render(); return; }
+ try { localStorage.setItem(VAULT_KEY, JSON.stringify(encryptVault(list, p.v1 || ''))); } catch {}
+ if (vaultPassword != null) vaultPassword = p.v1 || ''; // keep the unlocked session in sync
+ ui.pw = null;
+ render();
+ toast(t('pwChanged'));
+ return;
+ }
if (p.mode === 'set') {
// Password is optional — a blank one just persists without protection.
if (p.v1 !== p.v2) { p.error = t('pwMismatch'); render(); return; }
@@ -1934,6 +1951,7 @@ function accountsScreen() {
})
),
h('button', { class: 'btn-block', onClick: () => { ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.fromWallet = true; ui.unlockError = ''; render(); } }, t('addWallet')),
+ hasVault() ? h('button', { class: 'btn-ghost btn-block', onClick: startChangePw }, t('changePassword')) : null,
h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.confirmClear = true; render(); } }, t('clearAll'))
),
h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.screen = 'wallet'; render(); } }, t('back'))
@@ -1963,15 +1981,18 @@ function clearAll() {
function pwPromptCard() {
const p = ui.pw;
+ const change = p.purpose === 'change';
+ const newish = change || p.mode === 'set'; // entering a (new) password with confirm
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: p.mode === 'set' ? t('passwordOptional') : 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,
+ h('h3', {}, change ? t('changePassword') : p.mode === 'set' ? t('setPassword') : t('enterPassword')),
+ h('p', { class: 'small muted', style: 'margin:0' }, change ? t('changePasswordDesc') : p.mode === 'set' ? t('setPasswordDesc') : t('enterPasswordDesc')),
+ change ? h('input', { type: 'password', placeholder: t('currentPassword'), value: p.v0, onInput: (e) => (p.v0 = e.target.value) }) : null,
+ h('input', { type: 'password', placeholder: newish ? t('passwordOptional') : t('password'), value: p.v1, onInput: (e) => (p.v1 = e.target.value) }),
+ newish ? 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'))
+ h('button', { class: 'btn-primary grow', onClick: submitPw }, newish ? t('save') : t('unlock'))
)
);
}
diff --git a/src/i18n.js b/src/i18n.js
index 74ac0b4..84ef7b3 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -69,6 +69,10 @@ const STR = {
setPassword: 'Set a password (optional)',
setPasswordDesc: 'Encrypts this wallet on this device — leave it blank to save without a password. You will need it to unlock saved wallets later.',
passwordOptional: 'Password (optional)',
+ changePassword: 'Change password',
+ changePasswordDesc: 'Re-encrypt your saved-to-device wallets with a new password. Leave the new password blank to remove it.',
+ currentPassword: 'Current password',
+ pwChanged: 'Password changed',
enterPassword: 'Enter your password',
enterPasswordDesc: 'Unlock your saved wallets to continue.',
password: 'Password',