aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-17 08:12:34 -0700
committerAdam Soltys <[email protected]>2026-08-17 08:12:34 -0700
commitb62ca1099ff996e333015142cb712687737d4637 (patch)
treeb385b675aefbf9718f8774fe0e94bda41256a614 /src
parentf6120c49d80e0c8f2103a9adfd928f7004645aaf (diff)
downloadcoinosv3-b62ca1099ff996e333015142cb712687737d4637.tar.gz
coinosv3-b62ca1099ff996e333015142cb712687737d4637.zip
The seed takes dice: an optional entropy field folds open on the generate step, hashed with the device's own randomness so it can only strengthen the phrase — and the forget option now says all data, which is what it takes
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
Diffstat (limited to 'src')
-rw-r--r--src/app.js20
-rw-r--r--src/i18n.js5
-rw-r--r--src/wallet.js14
3 files changed, 33 insertions, 6 deletions
diff --git a/src/app.js b/src/app.js
index d4d13b8..63b638b 100644
--- a/src/app.js
+++ b/src/app.js
@@ -770,12 +770,28 @@ function createPane() {
'div',
{ class: 'col' },
h('p', { class: 'muted' }, t('genIntro')),
+ // BYO randomness for the seed — folded away so the common path stays
+ // one button. Whatever is typed here is hashed together with the
+ // device's own randomness (see newMnemonic), never used alone.
+ h('button', {
+ class: 'linklike small', style: 'align-self:flex-start',
+ onClick: () => { ui.ownEntropyOpen = !ui.ownEntropyOpen; render(); },
+ }, (ui.ownEntropyOpen ? '▾ ' : '▸ ') + t('entropyToggle')),
+ ui.ownEntropyOpen
+ ? h('div', { class: 'col gap6' },
+ h('p', { class: 'small muted', style: 'margin:0' }, t('entropyHint')),
+ h('textarea', {
+ rows: '2', style: 'font-family:var(--sans)', placeholder: t('entropyPlaceholder'),
+ autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false',
+ value: ui.ownEntropy || '', onInput: (e) => (ui.ownEntropy = e.target.value),
+ }))
+ : null,
h(
'button',
{
class: 'btn-primary btn-block',
onClick: () => {
- ui.draftMnemonic = newMnemonic();
+ ui.draftMnemonic = newMnemonic(128, (ui.ownEntropy || '').trim());
render();
},
},
@@ -804,7 +820,7 @@ function createPane() {
{
class: 'btn-ghost btn-sm',
onClick: () => {
- ui.draftMnemonic = newMnemonic();
+ ui.draftMnemonic = newMnemonic(128, (ui.ownEntropy || '').trim());
render();
},
},
diff --git a/src/i18n.js b/src/i18n.js
index d9dcdfb..0cd58ec 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -39,7 +39,10 @@ const STR = {
accounts: 'Wallets',
addWallet: 'Add wallet',
clearAll: 'Delete all',
- logoutForget: 'Log out & forget this device',
+ logoutForget: 'Logout & forget all data',
+ entropyToggle: 'Use my own entropy',
+ entropyHint: 'Type anything random — dice rolls, coin flips, shuffled cards. It gets mixed with this device’s own randomness, so it can only make the seed stronger.',
+ entropyPlaceholder: '4 2 6 6 1 3 5 2 …',
logoutPopBlurb: 'Your wallets stay saved on this device, one unlock away.',
clearAllWarn: '⚠ This removes every wallet from this device. Any wallet whose recovery phrase or key you have not backed up will be lost for good.',
removeWalletTitle: 'Remove wallet',
diff --git a/src/wallet.js b/src/wallet.js
index 662ef0f..2ede69d 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -9,7 +9,7 @@
// transactions, only the (txid, vout, value, address) of each UTXO.
import { HDKey } from '@scure/bip32';
-import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
+import { generateMnemonic, mnemonicToSeedSync, validateMnemonic, entropyToMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { hex, base64urlnopad, base32nopad, base58check } from '@scure/base';
import { sha256 } from '@noble/hashes/sha256';
@@ -64,8 +64,16 @@ export const NETS = {
regtest: { net: { ...btc.TEST_NETWORK, bech32: 'bcrt' }, coin: 1 },
};
-export function newMnemonic(strengthBits = 128) {
- return generateMnemonic(wordlist, strengthBits);
+export function newMnemonic(strengthBits = 128, extraEntropy = '') {
+ if (!extraEntropy) return generateMnemonic(wordlist, strengthBits);
+ // User entropy is MIXED with the CSPRNG, never used alone: hashing the two
+ // together can only add randomness — weak dice can't weaken the seed, and
+ // a distrusted device RNG can't fully determine it either.
+ const txt = new TextEncoder().encode(extraEntropy);
+ const rnd = crypto.getRandomValues(new Uint8Array(strengthBits / 8));
+ const mix = new Uint8Array(txt.length + rnd.length);
+ mix.set(txt); mix.set(rnd, txt.length);
+ return entropyToMnemonic(sha256(mix).slice(0, strengthBits / 8), wordlist);
}
// The base localStorage cache key for a wallet identity (xpub, xprv, or