Clarify password role, random die, updated wording
All checks were successful
check / check (push) Successful in 14s

- Password help text now explains it encrypts the recovery phrase
  on disk and is not used for address derivation
- Die button generates cryptographically random phrases using
  crypto.getRandomValues(), different each click
- "roll the die for a new one" wording
- README documents full encryption scheme (PBKDF2 + AES-256-GCM)
  and explicitly notes password is not part of BIP-39 derivation
This commit is contained in:
2026-02-25 15:34:33 +07:00
parent 3dbf885951
commit e6d8f6acf4
3 changed files with 170 additions and 26 deletions

View File

@@ -201,12 +201,22 @@ want maximum privacy can point it at their own Ethereum node.
- **No framework**: The popup UI is vanilla JS and HTML. The extension is small
enough that a framework adds unnecessary complexity and attack surface.
- **Encrypted storage**: Seed phrases are encrypted with a user-provided
password using AES-256-GCM before being stored in the extension's local
storage. The encryption key is derived from the password using PBKDF2 with a
high iteration count.
- **Encrypted storage**: Recovery phrases and private keys are encrypted at rest
in the extension's local storage. The encryption scheme:
- The user's password is run through PBKDF2-SHA256 (600,000 iterations) with
a random salt to derive a 256-bit encryption key.
- The encryption key + a random IV encrypt the secret material using
AES-256-GCM.
- Stored blob: `{ salt, iv, ciphertext, authTag }`.
- **The password is NOT used in address derivation.** It exists solely to
protect the recovery phrase / private key on disk. Anyone with the
recovery phrase can restore the wallet on any device without this
password. This matches MetaMask's behavior.
- **BIP-39 / BIP-44**: Standard mnemonic generation and HD key derivation
(`m/44'/60'/0'/0/n`) for Ethereum address compatibility.
(`m/44'/60'/0'/0/n`) for Ethereum address compatibility. The BIP-39 passphrase
is always empty (matching MetaMask and most wallet software). The user's
password is completely separate and has no effect on which addresses are
generated.
- **EIP-1193 provider**: The content script injects a `window.ethereum` object
that implements the EIP-1193 provider interface, enabling web3 site
connectivity.

View File

@@ -56,8 +56,8 @@
Add Wallet
</h1>
<p class="mb-2">
Enter your 12 or 24 word recovery phrase below, or press the
die to generate a new one.
Enter your 12 or 24 word recovery phrase below, or click the
button to roll the die for a new one.
</p>
<div class="mb-1 flex justify-end">
<button
@@ -88,8 +88,10 @@
<div class="mb-2" id="add-wallet-password-section">
<label class="block mb-1">Choose a password</label>
<p class="text-xs text-muted mb-1">
This password locks the wallet on this device. It is not
the same as your recovery phrase.
This password encrypts your recovery phrase on this
device. It does not affect your wallet addresses or
funds — anyone with your recovery phrase can restore
your wallet without this password.
</p>
<input
type="password"
@@ -154,7 +156,9 @@
<div class="mb-2" id="import-key-password-section">
<label class="block mb-1">Choose a password</label>
<p class="text-xs text-muted mb-1">
This password locks the wallet on this device.
This password encrypts your private key on this device.
Anyone with your private key can access your funds
without this password.
</p>
<input
type="password"

View File

@@ -70,9 +70,9 @@ function makeStubAddress() {
};
}
function generateStubMnemonic() {
// TODO: replace with real BIP-39 generation via background
const words = [
// Stub wordlist for random phrase generation.
// TODO: replace with real BIP-39 generation via background.
const STUB_WORDLIST = [
"abandon",
"ability",
"able",
@@ -85,8 +85,138 @@ function generateStubMnemonic() {
"abuse",
"access",
"accident",
];
return words.join(" ");
"account",
"accuse",
"achieve",
"acid",
"acoustic",
"acquire",
"across",
"act",
"action",
"actor",
"actual",
"adapt",
"add",
"addict",
"address",
"adjust",
"admit",
"adult",
"advance",
"advice",
"aerobic",
"affair",
"afford",
"afraid",
"again",
"age",
"agent",
"agree",
"ahead",
"aim",
"air",
"airport",
"aisle",
"alarm",
"album",
"alcohol",
"alert",
"alien",
"all",
"alley",
"allow",
"almost",
"alone",
"alpha",
"already",
"also",
"alter",
"always",
"amateur",
"amazing",
"among",
"amount",
"amused",
"analyst",
"anchor",
"ancient",
"anger",
"angle",
"angry",
"animal",
"ankle",
"announce",
"annual",
"another",
"answer",
"antenna",
"antique",
"anxiety",
"any",
"apart",
"apology",
"appear",
"apple",
"approve",
"april",
"arch",
"arctic",
"area",
"arena",
"argue",
"arm",
"armed",
"armor",
"army",
"around",
"arrange",
"arrest",
"arrive",
"arrow",
"art",
"artefact",
"artist",
"artwork",
"ask",
"aspect",
"assault",
"asset",
"assist",
"assume",
"asthma",
"athlete",
"atom",
"attack",
"attend",
"attitude",
"attract",
"auction",
"audit",
"august",
"aunt",
"author",
"auto",
"autumn",
"average",
"avocado",
"avoid",
"awake",
"aware",
"awesome",
"awful",
"awkward",
"axis",
];
function generateStubMnemonic() {
const phrase = [];
for (let i = 0; i < 12; i++) {
const bytes = new Uint32Array(1);
crypto.getRandomValues(bytes);
phrase.push(STUB_WORDLIST[bytes[0] % STUB_WORDLIST.length]);
}
return phrase.join(" ");
}
// -- render wallet list on main view --