Implement personal_sign and eth_signTypedData_v4 message signing
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
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.
This commit is contained in:
@@ -6,6 +6,7 @@ const {
|
|||||||
ETHEREUM_MAINNET_CHAIN_ID,
|
ETHEREUM_MAINNET_CHAIN_ID,
|
||||||
DEFAULT_RPC_URL,
|
DEFAULT_RPC_URL,
|
||||||
} = require("../shared/constants");
|
} = require("../shared/constants");
|
||||||
|
const { getBytes } = require("ethers");
|
||||||
const { state, loadState, saveState } = require("../shared/state");
|
const { state, loadState, saveState } = require("../shared/state");
|
||||||
const { refreshBalances, getProvider } = require("../shared/balances");
|
const { refreshBalances, getProvider } = require("../shared/balances");
|
||||||
const { debugFetch } = require("../shared/log");
|
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) => {
|
runtime.onConnect.addListener((port) => {
|
||||||
if (port.name.startsWith("approval:")) {
|
if (port.name.startsWith("approval:")) {
|
||||||
const id = parseInt(port.name.split(":")[1], 10);
|
const id = parseInt(port.name.split(":")[1], 10);
|
||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
const approval = pendingApprovals[id];
|
const approval = pendingApprovals[id];
|
||||||
if (approval) {
|
if (approval) {
|
||||||
|
if (approval.type === "sign") {
|
||||||
|
// Keep pending — user can reopen the toolbar popup
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (approval.type === "tx") {
|
if (approval.type === "tx") {
|
||||||
approval.resolve({
|
approval.resolve({
|
||||||
error: {
|
error: {
|
||||||
@@ -390,18 +429,59 @@ async function handleRpc(method, params, origin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method === "personal_sign" || method === "eth_sign") {
|
if (method === "personal_sign" || method === "eth_sign") {
|
||||||
return {
|
const s = await getState();
|
||||||
error: { message: "Signing not yet implemented in AutistMask." },
|
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") {
|
if (method === "eth_signTypedData_v4" || method === "eth_signTypedData") {
|
||||||
return {
|
const s = await getState();
|
||||||
error: {
|
const activeAddress = await getActiveAddress();
|
||||||
message:
|
if (!activeAddress)
|
||||||
"Typed data signing not yet implemented in AutistMask.",
|
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") {
|
if (method === "eth_sendTransaction") {
|
||||||
@@ -514,7 +594,7 @@ if (windowsApi && windowsApi.onRemoved) {
|
|||||||
windowsApi.onRemoved.addListener((windowId) => {
|
windowsApi.onRemoved.addListener((windowId) => {
|
||||||
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
for (const [id, approval] of Object.entries(pendingApprovals)) {
|
||||||
if (approval.windowId === windowId) {
|
if (approval.windowId === windowId) {
|
||||||
if (approval.type === "tx") {
|
if (approval.type === "tx" || approval.type === "sign") {
|
||||||
approval.resolve({
|
approval.resolve({
|
||||||
error: {
|
error: {
|
||||||
code: 4001,
|
code: 4001,
|
||||||
@@ -550,6 +630,10 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
resp.type = "tx";
|
resp.type = "tx";
|
||||||
resp.txParams = approval.txParams;
|
resp.txParams = approval.txParams;
|
||||||
}
|
}
|
||||||
|
if (approval.type === "sign") {
|
||||||
|
resp.type = "sign";
|
||||||
|
resp.signParams = approval.signParams;
|
||||||
|
}
|
||||||
sendResponse(resp);
|
sendResponse(resp);
|
||||||
} else {
|
} else {
|
||||||
sendResponse(null);
|
sendResponse(null);
|
||||||
@@ -624,6 +708,76 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return true;
|
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") {
|
if (msg.type === "AUTISTMASK_ACTIVE_CHANGED") {
|
||||||
broadcastAccountsChanged();
|
broadcastAccountsChanged();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -917,6 +917,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SIGNATURE APPROVAL ============ -->
|
||||||
|
<div id="view-approve-sign" class="view hidden">
|
||||||
|
<h2 class="font-bold mb-2">Signature Request</h2>
|
||||||
|
<p class="mb-2">
|
||||||
|
<span id="approve-sign-hostname" class="font-bold"></span>
|
||||||
|
wants you to sign a message.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-xs text-muted mb-1">Type</div>
|
||||||
|
<div id="approve-sign-type" class="text-xs font-bold"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-xs text-muted mb-1">From</div>
|
||||||
|
<div id="approve-sign-from" class="text-xs break-all"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="text-xs text-muted mb-1">Message</div>
|
||||||
|
<div
|
||||||
|
id="approve-sign-message"
|
||||||
|
class="text-xs break-all"
|
||||||
|
style="max-height: 12rem; overflow-y: auto"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="block mb-1 text-xs">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="approve-sign-password"
|
||||||
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="approve-sign-error" class="text-xs hidden mb-2"></div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<button
|
||||||
|
id="btn-approve-sign"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Sign
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="btn-reject-sign"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============ SITE APPROVAL ============ -->
|
<!-- ============ SITE APPROVAL ============ -->
|
||||||
<div id="view-approve-site" class="view hidden">
|
<div id="view-approve-site" class="view hidden">
|
||||||
<h2 class="font-bold mb-2">Connection Request</h2>
|
<h2 class="font-bold mb-2">Connection Request</h2>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
|
const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { formatEther, formatUnits, Interface } = require("ethers");
|
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
|
||||||
const { ERC20_ABI } = require("../../shared/constants");
|
const { ERC20_ABI } = require("../../shared/constants");
|
||||||
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
|
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
|
||||||
const txStatus = require("./txStatus");
|
const txStatus = require("./txStatus");
|
||||||
@@ -212,6 +212,86 @@ function showTxApproval(details) {
|
|||||||
showView("approve-tx");
|
showView("approve-tx");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeHexMessage(hex) {
|
||||||
|
try {
|
||||||
|
const bytes = Uint8Array.from(
|
||||||
|
hex
|
||||||
|
.slice(2)
|
||||||
|
.match(/.{1,2}/g)
|
||||||
|
.map((b) => parseInt(b, 16)),
|
||||||
|
);
|
||||||
|
return toUtf8String(bytes);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTypedDataHtml(jsonStr) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(jsonStr);
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
if (data.domain) {
|
||||||
|
html += `<div class="mb-2"><div class="text-muted">Domain</div>`;
|
||||||
|
for (const [key, val] of Object.entries(data.domain)) {
|
||||||
|
html += `<div><span class="text-muted">${escapeHtml(key)}:</span> ${escapeHtml(String(val))}</div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.primaryType) {
|
||||||
|
html += `<div class="mb-2"><div class="text-muted">Primary type</div>`;
|
||||||
|
html += `<div class="font-bold">${escapeHtml(data.primaryType)}</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
html += `<div class="mb-2"><div class="text-muted">Message</div>`;
|
||||||
|
for (const [key, val] of Object.entries(data.message)) {
|
||||||
|
const display =
|
||||||
|
typeof val === "object" ? JSON.stringify(val) : String(val);
|
||||||
|
html += `<div><span class="text-muted">${escapeHtml(key)}:</span> <span class="break-all">${escapeHtml(display)}</span></div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch {
|
||||||
|
return `<div class="break-all">${escapeHtml(jsonStr)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSignApproval(details) {
|
||||||
|
const sp = details.signParams;
|
||||||
|
|
||||||
|
$("approve-sign-hostname").textContent = details.hostname;
|
||||||
|
$("approve-sign-from").innerHTML = approvalAddressHtml(sp.from);
|
||||||
|
|
||||||
|
const isTyped =
|
||||||
|
sp.method === "eth_signTypedData_v4" ||
|
||||||
|
sp.method === "eth_signTypedData";
|
||||||
|
$("approve-sign-type").textContent = isTyped
|
||||||
|
? "Typed data (EIP-712)"
|
||||||
|
: "Personal message";
|
||||||
|
|
||||||
|
if (isTyped) {
|
||||||
|
$("approve-sign-message").innerHTML = formatTypedDataHtml(sp.typedData);
|
||||||
|
} else {
|
||||||
|
const decoded = decodeHexMessage(sp.message);
|
||||||
|
if (decoded !== null) {
|
||||||
|
$("approve-sign-message").textContent = decoded;
|
||||||
|
} else {
|
||||||
|
$("approve-sign-message").textContent = sp.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$("approve-sign-password").value = "";
|
||||||
|
$("approve-sign-error").classList.add("hidden");
|
||||||
|
$("btn-approve-sign").disabled = false;
|
||||||
|
$("btn-approve-sign").classList.remove("text-muted");
|
||||||
|
|
||||||
|
showView("approve-sign");
|
||||||
|
}
|
||||||
|
|
||||||
function show(id) {
|
function show(id) {
|
||||||
approvalId = id;
|
approvalId = id;
|
||||||
runtime.connect({ name: "approval:" + id });
|
runtime.connect({ name: "approval:" + id });
|
||||||
@@ -224,6 +304,10 @@ function show(id) {
|
|||||||
showTxApproval(details);
|
showTxApproval(details);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (details.type === "sign") {
|
||||||
|
showSignApproval(details);
|
||||||
|
return;
|
||||||
|
}
|
||||||
$("approve-hostname").textContent = details.hostname;
|
$("approve-hostname").textContent = details.hostname;
|
||||||
$("approve-address").innerHTML = approvalAddressHtml(
|
$("approve-address").innerHTML = approvalAddressHtml(
|
||||||
state.activeAddress,
|
state.activeAddress,
|
||||||
@@ -301,6 +385,48 @@ function init(ctx) {
|
|||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("btn-approve-sign").addEventListener("click", () => {
|
||||||
|
const password = $("approve-sign-password").value;
|
||||||
|
if (!password) {
|
||||||
|
$("approve-sign-error").textContent = "Please enter your password.";
|
||||||
|
$("approve-sign-error").classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("approve-sign-error").classList.add("hidden");
|
||||||
|
$("btn-approve-sign").disabled = true;
|
||||||
|
$("btn-approve-sign").classList.add("text-muted");
|
||||||
|
|
||||||
|
runtime.sendMessage(
|
||||||
|
{
|
||||||
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
||||||
|
id: approvalId,
|
||||||
|
approved: true,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
if (response && response.signature) {
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
const msg =
|
||||||
|
(response && response.error) || "Signing failed.";
|
||||||
|
$("approve-sign-error").textContent = msg;
|
||||||
|
$("approve-sign-error").classList.remove("hidden");
|
||||||
|
$("btn-approve-sign").disabled = false;
|
||||||
|
$("btn-approve-sign").classList.remove("text-muted");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btn-reject-sign").addEventListener("click", () => {
|
||||||
|
runtime.sendMessage({
|
||||||
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
||||||
|
id: approvalId,
|
||||||
|
approved: false,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, show };
|
module.exports = { init, show };
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const VIEWS = [
|
|||||||
"transaction",
|
"transaction",
|
||||||
"approve-site",
|
"approve-site",
|
||||||
"approve-tx",
|
"approve-tx",
|
||||||
|
"approve-sign",
|
||||||
];
|
];
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user