From 71f1d11a33e8185cce56fc84a46199e62a833df7 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 27 Feb 2026 14:55:11 +0700 Subject: [PATCH] Implement personal_sign and eth_signTypedData_v4 message signing Replace stub error handlers with full approval flow for personal_sign, eth_sign, eth_signTypedData_v4, and eth_signTypedData. Uses toolbar popup only (no fallback window) and keeps sign approvals pending across popup close/reopen cycles so the user can respond via the toolbar icon. --- src/background/index.js | 176 +++++++++++++++++++++++++++++++++--- src/popup/index.html | 52 +++++++++++ src/popup/views/approval.js | 128 +++++++++++++++++++++++++- src/popup/views/helpers.js | 1 + 4 files changed, 345 insertions(+), 12 deletions(-) diff --git a/src/background/index.js b/src/background/index.js index 16f97e6..33ab87b 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -6,6 +6,7 @@ const { ETHEREUM_MAINNET_CHAIN_ID, DEFAULT_RPC_URL, } = require("../shared/constants"); +const { getBytes } = require("ethers"); const { state, loadState, saveState } = require("../shared/state"); const { refreshBalances, getProvider } = require("../shared/balances"); const { debugFetch } = require("../shared/log"); @@ -177,13 +178,51 @@ function requestTxApproval(origin, hostname, txParams) { }); } -// Detect when an approval popup (browser-action) closes without a response +// Open a sign-approval popup and return a promise that resolves with { signature } or { error }. +// Uses the toolbar popup only — no fallback window. If openPopup() fails the +// popup URL is still set, so the user can click the toolbar icon to respond. +function requestSignApproval(origin, hostname, signParams) { + return new Promise((resolve) => { + const id = nextApprovalId++; + pendingApprovals[id] = { + origin, + hostname, + signParams, + resolve, + type: "sign", + }; + + if (actionApi && typeof actionApi.setPopup === "function") { + actionApi.setPopup({ + popup: "src/popup/index.html?approval=" + id, + }); + } + if (actionApi && typeof actionApi.openPopup === "function") { + try { + const result = actionApi.openPopup(); + if (result && typeof result.catch === "function") { + result.catch(() => {}); + } + } catch { + // openPopup unsupported — user clicks toolbar icon + } + } + }); +} + +// Detect when an approval popup (browser-action) closes without a response. +// Sign approvals are NOT auto-rejected on disconnect because toolbar popups +// naturally close on focus loss and the user can reopen them. runtime.onConnect.addListener((port) => { if (port.name.startsWith("approval:")) { const id = parseInt(port.name.split(":")[1], 10); port.onDisconnect.addListener(() => { const approval = pendingApprovals[id]; if (approval) { + if (approval.type === "sign") { + // Keep pending — user can reopen the toolbar popup + return; + } if (approval.type === "tx") { approval.resolve({ error: { @@ -390,18 +429,59 @@ async function handleRpc(method, params, origin) { } if (method === "personal_sign" || method === "eth_sign") { - return { - error: { message: "Signing not yet implemented in AutistMask." }, - }; + const s = await getState(); + const activeAddress = await getActiveAddress(); + if (!activeAddress) + return { error: { message: "No accounts available" } }; + + const hostname = extractHostname(origin); + const allowed = s.allowedSites[activeAddress] || []; + if ( + !allowed.includes(hostname) && + !connectedSites[origin + ":" + activeAddress] + ) { + return { error: { code: 4100, message: "Unauthorized" } }; + } + + // personal_sign: params[0]=message, params[1]=address + // eth_sign: params[0]=address, params[1]=message + const signParams = + method === "personal_sign" + ? { method, message: params[0], from: params[1] } + : { method, message: params[1], from: params[0] }; + + const decision = await requestSignApproval( + origin, + hostname, + signParams, + ); + if (decision.error) return { error: decision.error }; + return { result: decision.signature }; } if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") { - return { - error: { - message: - "Typed data signing not yet implemented in AutistMask.", - }, - }; + const s = await getState(); + const activeAddress = await getActiveAddress(); + if (!activeAddress) + return { error: { message: "No accounts available" } }; + + const hostname = extractHostname(origin); + const allowed = s.allowedSites[activeAddress] || []; + if ( + !allowed.includes(hostname) && + !connectedSites[origin + ":" + activeAddress] + ) { + return { error: { code: 4100, message: "Unauthorized" } }; + } + + const signParams = { method, typedData: params[1], from: params[0] }; + const decision = await requestSignApproval( + origin, + hostname, + signParams, + ); + if (decision.error) return { error: decision.error }; + return { result: decision.signature }; } if (method === "eth_sendTransaction") { @@ -514,7 +594,7 @@ if (windowsApi && windowsApi.onRemoved) { windowsApi.onRemoved.addListener((windowId) => { for (const [id, approval] of Object.entries(pendingApprovals)) { if (approval.windowId === windowId) { - if (approval.type === "tx") { + if (approval.type === "tx" || approval.type === "sign") { approval.resolve({ error: { code: 4001, @@ -550,6 +630,10 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => { resp.type = "tx"; resp.txParams = approval.txParams; } + if (approval.type === "sign") { + resp.type = "sign"; + resp.signParams = approval.signParams; + } sendResponse(resp); } else { sendResponse(null); @@ -624,6 +708,76 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => { return true; } + if (msg.type === "AUTISTMASK_SIGN_RESPONSE") { + const approval = pendingApprovals[msg.id]; + if (!approval) return false; + delete pendingApprovals[msg.id]; + resetPopupUrl(); + + if (!msg.approved) { + approval.resolve({ + error: { code: 4001, message: "User rejected the request." }, + }); + return true; + } + + (async () => { + try { + await loadState(); + const activeAddress = await getActiveAddress(); + let wallet, addrIndex; + for (const w of state.wallets) { + for (let i = 0; i < w.addresses.length; i++) { + if (w.addresses[i].address === activeAddress) { + wallet = w; + addrIndex = i; + break; + } + } + if (wallet) break; + } + if (!wallet) throw new Error("Wallet not found"); + const decrypted = await decryptWithPassword( + wallet.encryptedSecret, + msg.password, + ); + const signer = getSignerForAddress( + wallet, + addrIndex, + decrypted, + ); + + const sp = approval.signParams; + let signature; + + if (sp.method === "personal_sign" || sp.method === "eth_sign") { + signature = await signer.signMessage(getBytes(sp.message)); + } else { + // eth_signTypedData_v4 / eth_signTypedData + const typedData = JSON.parse(sp.typedData); + const { domain, types, message } = typedData; + // ethers handles EIP712Domain internally + delete types.EIP712Domain; + signature = await signer.signTypedData( + domain, + types, + message, + ); + } + + approval.resolve({ signature }); + sendResponse({ signature }); + } catch (e) { + const errMsg = e.shortMessage || e.message; + approval.resolve({ + error: { message: errMsg }, + }); + sendResponse({ error: errMsg }); + } + })(); + return true; + } + if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") { broadcastAccountsChanged(); return false; diff --git a/src/popup/index.html b/src/popup/index.html index 534881f..d382356 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -917,6 +917,58 @@ + + +