diff --git a/README.md b/README.md
index 0fce9ab..0b92f75 100644
--- a/README.md
+++ b/README.md
@@ -151,33 +151,29 @@ menus.
The popup has the following views, switched via simple show/hide:
-1. **Lock**: Password input + Unlock button. Shown when the wallet is locked or
- on first open after browser restart.
-2. **Welcome**: Shown on first use. Two options: "Add wallet" (recovery phrase
- based) and "Import private key". Password is set during the first wallet
- addition.
-3. **Add wallet**: A unified view for both creating and importing recovery
+1. **Welcome**: Shown on first use. Single "Add wallet" button.
+2. **Add wallet**: A unified view for both creating and importing recovery
phrase wallets. The recovery phrase text area starts empty. A clickable die
button `[die]` generates a random 12-word phrase and fills it in. If the user
already has a phrase, they paste it directly. When the die is clicked, a
- warning box appears reminding the user to write the phrase down. Password
- fields are shown only on first use.
-4. **Import private key**: Paste a private key. This creates a wallet with a
- single address. Password fields shown only on first use.
-5. **Main**: All wallets listed, each showing its addresses with truncated
- address and ETH balance. "+" next to recovery phrase wallets to add another
- address. "+ Add wallet" and "+ Import private key" buttons at the bottom.
- Settings and Lock buttons in the header. Future: a sub-heading showing total
- portfolio value in USD (and eventually other currencies).
-6. **Address detail**: Full address (click to copy), ETH balance, USD value
+ warning box appears reminding the user to write the phrase down. No password
+ required — the xpub is derived and stored for read-only access.
+3. **Import private key**: Paste a private key. This creates a wallet with a
+ single address.
+4. **Main**: All wallets listed, each showing its addresses (full, untruncated)
+ and ETH balance. "+" next to recovery phrase wallets to add another address.
+ "+ Add wallet" at the bottom. Settings button in the header. Future: a
+ sub-heading showing total portfolio value in USD (and eventually other
+ currencies).
+5. **Address detail**: Full address (click to copy), ETH balance, USD value
(future), Send/Receive buttons, token list with "+ Add" button.
-7. **Send**: Token selector, recipient address, amount. Cancel returns to
+6. **Send**: Token selector, recipient address, amount. Cancel returns to
address detail.
-8. **Receive**: Full address displayed with "Copy address" button.
-9. **Add token**: Enter contract address. The extension looks up the token
+7. **Receive**: Full address displayed with "Copy address" button.
+8. **Add token**: Enter contract address. The extension looks up the token
name/symbol automatically.
-10. **Settings**: Network (RPC endpoint URL) with explanatory text.
-11. **Approval**: When a website requests wallet access or a signature, shows
+9. **Settings**: Network (RPC endpoint URL) with explanatory text.
+10. **Approval**: When a website requests wallet access or a signature, shows
the site origin, request details, and Allow/Deny buttons.
### External Services
@@ -256,8 +252,12 @@ project owner.
- **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**: Recovery phrases and private keys are encrypted at rest
- in the extension's local storage using libsodium. The encryption scheme:
+- **Split storage model**: Public data (xpubs, derived addresses, token lists,
+ balances) is stored unencrypted in extension local storage so the user can
+ view their wallets and balances at any time without entering a password.
+ Private data (recovery phrases, private keys) will be encrypted at rest using
+ libsodium — a password is only required when the user needs to sign a
+ transaction or message. The encryption scheme for private data:
- The user's password is run through Argon2id (`crypto_pwhash`) to derive a
256-bit encryption key. Argon2id is memory-hard, making GPU/ASIC brute
force attacks expensive.
diff --git a/src/popup/index.html b/src/popup/index.html
index 81cff3d..78528cd 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -8,34 +8,6 @@
-
-
-
- AutistMask
-
-
- Your wallet is locked. Enter your password to continue.
-
-
-
-
-
-
-
-
-
@@ -85,28 +57,6 @@
can access your funds. If you lose them, your wallet cannot
be recovered.
-
-
-
- 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.
-
-
-
-
-
-
-
-
-
-
- This password encrypts your private key on this device.
- Anyone with your private key can access your funds
- without this password.
-
-
-
-
-
-
-
diff --git a/src/popup/index.js b/src/popup/index.js
index 5720d51..e349e35 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -1,12 +1,13 @@
// AutistMask popup UI — view management and event wiring
-const { Mnemonic } = require("ethers");
+const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
const DEBUG = true;
const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
+const BIP44_ETH_BASE = "m/44'/60'/0'/0";
+
const VIEWS = [
- "lock",
"welcome",
"add-wallet",
"import-key",
@@ -29,26 +30,22 @@ function showView(name) {
}
// Browser-agnostic storage API
-const storage =
+const storageApi =
typeof browser !== "undefined"
? browser.storage.local
: chrome.storage.local;
-// A wallet is either { type: "hd", name, mnemonic, addresses: [...] }
-// or { type: "key", name, privateKey, addresses: [single] }.
-// Each address is { address, balance, tokens: [...] }.
+// Persisted state (unencrypted, public data only):
+// wallets[]: { type, name, xpub (for hd), addresses: [{ address, balance, tokens }], nextIndex }
+// Mnemonic/private key will be stored encrypted separately (not yet implemented).
const DEFAULT_STATE = {
hasWallet: false,
wallets: [],
rpcUrl: "https://eth.llamarpc.com",
- isFirstSetup: true,
};
-// Transient state (not persisted)
const state = {
...DEFAULT_STATE,
- locked: true,
- password: null,
selectedWallet: null,
selectedAddress: null,
};
@@ -58,19 +55,17 @@ async function saveState() {
hasWallet: state.hasWallet,
wallets: state.wallets,
rpcUrl: state.rpcUrl,
- isFirstSetup: state.isFirstSetup,
};
- await storage.set({ autistmask: persisted });
+ await storageApi.set({ autistmask: persisted });
}
async function loadState() {
- const result = await storage.get("autistmask");
+ const result = await storageApi.get("autistmask");
if (result.autistmask) {
const saved = result.autistmask;
state.hasWallet = saved.hasWallet;
state.wallets = saved.wallets || [];
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
- state.isFirstSetup = saved.isFirstSetup;
}
}
@@ -89,28 +84,33 @@ function hideError(id) {
$(id).classList.add("hidden");
}
-function truncateAddress(addr) {
- if (!addr) return "";
- return addr.slice(0, 6) + "\u2026" + addr.slice(-4);
-}
-
-function makeStubAddress() {
- const hex = Array.from({ length: 40 }, () =>
- Math.floor(Math.random() * 16).toString(16),
- ).join("");
- return {
- address: "0x" + hex,
- balance: "0.0000",
- tokens: [],
- };
-}
-
function generateMnemonic() {
if (DEBUG) return DEBUG_MNEMONIC;
- const wallet = Mnemonic.fromEntropy(
+ const m = Mnemonic.fromEntropy(
globalThis.crypto.getRandomValues(new Uint8Array(16)),
);
- return wallet.phrase;
+ return m.phrase;
+}
+
+// Derive an Ethereum address at index from an xpub string
+function deriveAddressFromXpub(xpub, index) {
+ const node = HDNodeWallet.fromExtendedKey(xpub);
+ const child = node.deriveChild(index);
+ return child.address;
+}
+
+// Create an HD wallet from a mnemonic: returns { xpub, firstAddress }
+function hdWalletFromMnemonic(mnemonic) {
+ const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_BASE);
+ const xpub = node.neuter().extendedKey;
+ const firstAddress = node.deriveChild(0).address;
+ return { xpub, firstAddress };
+}
+
+// Get address from a private key
+function addressFromPrivateKey(key) {
+ const w = new Wallet(key);
+ return w.address;
}
// -- render wallet list on main view --
@@ -135,7 +135,7 @@ function renderWalletList() {
wallet.addresses.forEach((addr, ai) => {
html += `
`;
html += `${addr.address}`;
- html += `${addr.balance} ETH`;
+ html += `${addr.balance} ETH`;
html += `