Store xpubs unencrypted, remove password from viewing flow
All checks were successful
check / check (push) Successful in 12s
All checks were successful
check / check (push) Successful in 12s
Xpubs and derived addresses stored unencrypted in extension storage for instant read-only access without a password. Password will only be required for signing transactions (not yet implemented). Real addresses now derived from mnemonic via ethers HDNodeWallet at wallet creation time. Removed lock screen, password fields, and Lock button. BIP-39 mnemonic validation added. README updated with split storage model documentation.
This commit is contained in:
@@ -8,34 +8,6 @@
|
||||
</head>
|
||||
<body class="bg-bg text-fg font-mono text-sm">
|
||||
<div id="app" class="p-2">
|
||||
<!-- ============ LOCK SCREEN ============ -->
|
||||
<div id="view-lock" class="view hidden">
|
||||
<h1 class="font-bold border-b border-border pb-1 mb-3">
|
||||
AutistMask
|
||||
</h1>
|
||||
<p class="mb-3">
|
||||
Your wallet is locked. Enter your password to continue.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
<label class="block mb-1">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="unlock-password"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="btn-unlock"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
<div
|
||||
id="unlock-error"
|
||||
class="mt-2 border border-border border-dashed p-1 hidden"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- ============ WELCOME / FIRST USE ============ -->
|
||||
<div id="view-welcome" class="view hidden">
|
||||
<h1 class="font-bold border-b border-border pb-1 mb-3">
|
||||
@@ -85,28 +57,6 @@
|
||||
can access your funds. If you lose them, your wallet cannot
|
||||
be recovered.
|
||||
</div>
|
||||
<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 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"
|
||||
id="add-wallet-password"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2" id="add-wallet-password-confirm-section">
|
||||
<label class="block mb-1">Confirm password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="add-wallet-password-confirm"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-add-wallet-confirm"
|
||||
@@ -153,27 +103,6 @@
|
||||
placeholder="0x..."
|
||||
/>
|
||||
</div>
|
||||
<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 encrypts your private key on this device.
|
||||
Anyone with your private key can access your funds
|
||||
without this password.
|
||||
</p>
|
||||
<input
|
||||
type="password"
|
||||
id="import-key-password"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2" id="import-key-password-confirm-section">
|
||||
<label class="block mb-1">Confirm password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="import-key-password-confirm"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-import-key-confirm"
|
||||
@@ -200,22 +129,13 @@
|
||||
class="flex justify-between items-center border-b border-border pb-1 mb-2"
|
||||
>
|
||||
<h1 class="font-bold">AutistMask</h1>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="btn-settings"
|
||||
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
title="Settings"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
id="btn-lock"
|
||||
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
title="Lock wallet"
|
||||
>
|
||||
Lock
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="btn-settings"
|
||||
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
title="Settings"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- wallet list -->
|
||||
|
||||
@@ -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 += `<div class="address-row flex justify-between items-center py-1 border-b border-border-light cursor-pointer hover:bg-hover px-1" data-wallet="${wi}" data-address="${ai}">`;
|
||||
html += `<span class="text-xs break-all">${addr.address}</span>`;
|
||||
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
||||
html += `<span class="text-xs ml-1 whitespace-nowrap">${addr.balance} ETH</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
@@ -155,8 +155,17 @@ function renderWalletList() {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const wi = parseInt(btn.dataset.wallet, 10);
|
||||
// TODO: derive next address from seed via background
|
||||
state.wallets[wi].addresses.push(makeStubAddress());
|
||||
const wallet = state.wallets[wi];
|
||||
const newAddr = deriveAddressFromXpub(
|
||||
wallet.xpub,
|
||||
wallet.nextIndex,
|
||||
);
|
||||
wallet.addresses.push({
|
||||
address: newAddr,
|
||||
balance: "0.0000",
|
||||
tokens: [],
|
||||
});
|
||||
wallet.nextIndex++;
|
||||
await saveState();
|
||||
renderWalletList();
|
||||
});
|
||||
@@ -215,7 +224,6 @@ function currentAddress() {
|
||||
async function addWalletAndGoToMain(wallet) {
|
||||
state.wallets.push(wallet);
|
||||
state.hasWallet = true;
|
||||
state.isFirstSetup = false;
|
||||
await saveState();
|
||||
renderWalletList();
|
||||
showView("main");
|
||||
@@ -225,49 +233,17 @@ function showAddWalletView() {
|
||||
$("wallet-mnemonic").value = "";
|
||||
$("add-wallet-phrase-warning").classList.add("hidden");
|
||||
hideError("add-wallet-error");
|
||||
const needsPw = state.isFirstSetup;
|
||||
$("add-wallet-password-section").classList.toggle("hidden", !needsPw);
|
||||
$("add-wallet-password-confirm-section").classList.toggle(
|
||||
"hidden",
|
||||
!needsPw,
|
||||
);
|
||||
showView("add-wallet");
|
||||
}
|
||||
|
||||
function showImportKeyView() {
|
||||
$("import-private-key").value = "";
|
||||
hideError("import-key-error");
|
||||
const needsPw = state.isFirstSetup;
|
||||
$("import-key-password-section").classList.toggle("hidden", !needsPw);
|
||||
$("import-key-password-confirm-section").classList.toggle(
|
||||
"hidden",
|
||||
!needsPw,
|
||||
);
|
||||
showView("import-key");
|
||||
}
|
||||
|
||||
function validatePasswords(pwId, pw2Id, errorId) {
|
||||
if (!state.isFirstSetup) return true;
|
||||
const pw = $(pwId).value;
|
||||
const pw2 = $(pw2Id).value;
|
||||
if (!pw) {
|
||||
showError(errorId, "Please choose a password.");
|
||||
return false;
|
||||
}
|
||||
if (pw.length < 8) {
|
||||
showError(errorId, "Password must be at least 8 characters.");
|
||||
return false;
|
||||
}
|
||||
if (pw !== pw2) {
|
||||
showError(errorId, "Passwords do not match.");
|
||||
return false;
|
||||
}
|
||||
state.password = pw;
|
||||
return true;
|
||||
}
|
||||
|
||||
function backFromWalletAdd() {
|
||||
if (state.isFirstSetup) {
|
||||
if (!state.hasWallet) {
|
||||
showView("welcome");
|
||||
} else {
|
||||
renderWalletList();
|
||||
@@ -289,27 +265,11 @@ async function init() {
|
||||
|
||||
if (!state.hasWallet) {
|
||||
showView("welcome");
|
||||
} else if (state.locked) {
|
||||
showView("lock");
|
||||
} else {
|
||||
renderWalletList();
|
||||
showView("main");
|
||||
}
|
||||
|
||||
// -- Lock screen --
|
||||
$("btn-unlock").addEventListener("click", () => {
|
||||
const pw = $("unlock-password").value;
|
||||
if (!pw) {
|
||||
showError("unlock-error", "Please enter your password.");
|
||||
return;
|
||||
}
|
||||
hideError("unlock-error");
|
||||
// TODO: send unlock message to background
|
||||
state.locked = false;
|
||||
renderWalletList();
|
||||
showView("main");
|
||||
});
|
||||
|
||||
// -- Welcome --
|
||||
$("btn-welcome-add").addEventListener("click", showAddWalletView);
|
||||
|
||||
@@ -339,22 +299,26 @@ async function init() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!validatePasswords(
|
||||
"add-wallet-password",
|
||||
"add-wallet-password-confirm",
|
||||
// Validate the mnemonic is real BIP-39
|
||||
if (!Mnemonic.isValidMnemonic(mnemonic)) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
)
|
||||
) {
|
||||
"Invalid recovery phrase. Please check for typos.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
hideError("add-wallet-error");
|
||||
|
||||
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
||||
const walletNum = state.wallets.length + 1;
|
||||
addWalletAndGoToMain({
|
||||
type: "hd",
|
||||
name: "Wallet " + walletNum,
|
||||
mnemonic: mnemonic,
|
||||
addresses: [makeStubAddress()],
|
||||
xpub: xpub,
|
||||
nextIndex: 1,
|
||||
addresses: [
|
||||
{ address: firstAddress, balance: "0.0000", tokens: [] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -368,13 +332,11 @@ async function init() {
|
||||
showError("import-key-error", "Please enter your private key.");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!validatePasswords(
|
||||
"import-key-password",
|
||||
"import-key-password-confirm",
|
||||
"import-key-error",
|
||||
)
|
||||
) {
|
||||
let addr;
|
||||
try {
|
||||
addr = addressFromPrivateKey(key);
|
||||
} catch (e) {
|
||||
showError("import-key-error", "Invalid private key.");
|
||||
return;
|
||||
}
|
||||
hideError("import-key-error");
|
||||
@@ -382,20 +344,13 @@ async function init() {
|
||||
addWalletAndGoToMain({
|
||||
type: "key",
|
||||
name: "Wallet " + walletNum,
|
||||
privateKey: key,
|
||||
addresses: [makeStubAddress()],
|
||||
addresses: [{ address: addr, balance: "0.0000", tokens: [] }],
|
||||
});
|
||||
});
|
||||
|
||||
$("btn-import-key-back").addEventListener("click", backFromWalletAdd);
|
||||
|
||||
// -- Main view --
|
||||
$("btn-lock").addEventListener("click", () => {
|
||||
state.locked = true;
|
||||
$("unlock-password").value = "";
|
||||
showView("lock");
|
||||
});
|
||||
|
||||
$("btn-settings").addEventListener("click", () => {
|
||||
$("settings-rpc").value = state.rpcUrl;
|
||||
showView("settings");
|
||||
@@ -455,9 +410,9 @@ async function init() {
|
||||
$("send-status").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
// TODO: construct and send transaction via background
|
||||
// TODO: prompt for password, decrypt key, construct and send transaction
|
||||
const el = $("send-status");
|
||||
el.textContent = "Sent! (stub)";
|
||||
el.textContent = "Sent! (stub — password/signing not yet implemented)";
|
||||
el.classList.remove("hidden");
|
||||
});
|
||||
|
||||
@@ -488,7 +443,7 @@ async function init() {
|
||||
return;
|
||||
}
|
||||
hideError("add-token-error");
|
||||
// TODO: look up token name/symbol/decimals from contract via background
|
||||
// TODO: look up token name/symbol/decimals from contract via RPC
|
||||
const addr = currentAddress();
|
||||
if (addr) {
|
||||
addr.tokens.push({
|
||||
|
||||
Reference in New Issue
Block a user