harden: key remembered site permissions by full origin (closes #402)
check / check (push) Failing after 10s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s

allowedSites and deniedSites held the bare hostname, so a grant to
https://dapp.example also authorised http://dapp.example and every port
on that host, and the connection, transaction and signature prompts
named only the hostname. Both lists now store and match the full origin
(scheme://host[:port]), the key the connections approved without
Remember already used. The prompts, the Settings site lists and
AUTISTMASK_REMOVE_SITE use the origin too. Entries saved by hostname
are not migrated (pre-1.0): they match no site.

Model: opus-5-5
This commit is contained in:
2026-10-04 14:17:28 +00:00
parent 45f11ee920
commit f87c97ba50
21 changed files with 376 additions and 166 deletions
+140
View File
@@ -0,0 +1,140 @@
// The connection, transaction and signature prompts name the site by its full
// origin, scheme and port included, not by its bare hostname
// (https://git.eeqj.de/sneak/AutistMask/issues/402). A page served over http,
// or on another port, of a host the user trusts over https must not raise a
// prompt that reads as that trusted site.
//
// Driven against a minimal DOM stub in the shape
// tests/contractCreation.test.js uses.
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const { state } = require("../src/shared/state");
const approval = require("../src/popup/views/approval");
// The site asking, in cleartext and on a port, which is what the hostname
// alone, dapp.example, used to hide.
const ORIGIN = "http://dapp.example:8080";
const FROM = "0x0000000000000000000000000000000000000a11";
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
function makeElement(id) {
const classes = new Set();
const el = {
id,
textContent: "",
value: "",
innerHTML: "",
disabled: false,
style: {},
dataset: {},
classList: {
add: (...names) => names.forEach((n) => classes.add(n)),
remove: (...names) => names.forEach((n) => classes.delete(n)),
contains: (n) => classes.has(n),
toggle: (n, force) => {
const on = force === undefined ? !classes.has(n) : force;
if (on) classes.add(n);
else classes.delete(n);
return on;
},
},
addEventListener: () => {},
querySelectorAll: () => [],
appendChild: () => {},
};
// Views reach for .parentElement to hide whole sections.
Object.defineProperty(el, "parentElement", {
get: () => node(id + "-parent"),
});
return el;
}
function makeDocument() {
const els = new Map();
return {
getElementById(id) {
// The debug banner is created on demand by helpers.js; absent
// is the state a non-debug, non-testnet popup is in.
if (id === "debug-banner") return null;
if (!els.has(id)) els.set(id, makeElement(id));
return els.get(id);
},
createElement: () => makeElement("created"),
body: { prepend: () => {} },
};
}
function node(id) {
return globalThis.document.getElementById(id);
}
// Open the prompt the background describes with `details`, the way the popup
// does: it asks for the approval and show() draws it.
async function openApproval(details) {
globalThis.document = makeDocument();
globalThis.window = { location: { search: "" } };
globalThis.chrome.runtime = {
connect: () => ({ postMessage: () => {} }),
sendMessage: (msg, reply) => {
if (!reply) return;
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
reply({
origin: ORIGIN,
isPhishingDomain: false,
approvedFrom: FROM,
...details,
});
},
};
approval.init({});
await approval.show(1);
}
beforeEach(() => {
state.wallets = [];
state.activeAddress = FROM;
state.viewData = {};
state.viewStack = [];
state.currentView = null;
});
test("the connection prompt shows the origin", async () => {
await openApproval({});
expect(node("approve-origin").textContent).toBe(ORIGIN);
});
test("the transaction prompt shows the origin", async () => {
await openApproval({
type: "tx",
approvedTx: {
type: 2,
from: FROM,
chainId: "0x1",
nonce: "0x7",
gasLimit: "0x5208",
maxPriorityFeePerGas: "0x3b9aca00",
maxFeePerGas: "0x77359400",
to: RECIPIENT,
value: "0x0",
data: "0x",
accessList: [],
},
});
expect(node("approve-tx-origin").textContent).toBe(ORIGIN);
});
test("the signature prompt shows the origin", async () => {
await openApproval({
type: "sign",
// "Hello" as the hex a dApp passes to personal_sign.
signParams: {
method: "personal_sign",
message: "0x48656c6c6f",
from: FROM,
},
});
expect(node("approve-sign-origin").textContent).toBe(ORIGIN);
});
+85 -17
View File
@@ -39,7 +39,6 @@ const other = new Wallet(OTHER_KEY);
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const ORIGIN = "https://dapp.example";
const HOSTNAME = "dapp.example";
// A page the wallet has never been connected to, whose requests are refused.
const UNCONNECTED_ORIGIN = "https://stranger.example";
const EXT_URL = "chrome-extension://autistmask/";
@@ -199,7 +198,7 @@ function loadBackground(options) {
networkId: "mainnet",
rpcUrl: "https://rpc.invalid",
activeAddress: signer.address,
allowedSites: { [signer.address]: [HOSTNAME] },
allowedSites: { [signer.address]: [ORIGIN] },
deniedSites: {},
};
@@ -2193,12 +2192,54 @@ describe("removing an address ends a site's connection to it", () => {
});
});
// A remembered permission belongs to the origin it was granted to, scheme and
// port included (https://git.eeqj.de/sneak/AutistMask/issues/402). The stored
// state allows ORIGIN, https://dapp.example. A cleartext page on the same host,
// which a network attacker can serve, and another port on it are other sites.
describe("a remembered permission is held by the full origin", () => {
for (const origin of ["http://dapp.example", "https://dapp.example:8443"]) {
test(`an https grant does not authorise ${origin}`, async () => {
const bg = loadBackground();
expect(await siteAccounts(bg, ORIGIN)).toEqual({
result: [signer.address],
});
expect(await siteAccounts(bg, origin)).toEqual({ result: [] });
const send = bg.requestTx(TX_PARAMS, origin);
await settle();
expect(send.result()).toEqual({
error: { code: 4100, message: "Unauthorized" },
});
expect(send.id()).toBeNull();
});
}
test("Remember stores the origin, so the cleartext page on that host is asked again", async () => {
const bg = loadBackground({ actionPopup: true });
const granted = bg.requestSite(FRESH_ORIGIN);
await settle();
const grantedId = granted.id();
bg.connectApproval(grantedId).decide(true, true);
await settle();
expect(granted.result()).toEqual({ result: [signer.address] });
expect(
bg.storage.read("autistmask").allowedSites[signer.address],
).toEqual([ORIGIN, FRESH_ORIGIN]);
const cleartext = bg.requestSite("http://fresh.example");
await settle();
// Unanswered: it is waiting on a prompt of its own.
expect(cleartext.result()).toBeNull();
expect(cleartext.id()).not.toBe(grantedId);
});
});
// Settings lists the sites allowed without "Remember", which only the
// background holds, and removing a site there, from either list, disconnects
// it. These drive the real Settings view against the real background and
// click the [x] the user clicks.
describe("removing a site in Settings disconnects it", () => {
// FRESH_ORIGIN on another port, so its hostname is FRESH_ORIGIN's.
// The host of FRESH_ORIGIN on another port, which makes it another site.
const FRESH_OTHER_PORT = "https://fresh.example:8443";
// A site list's container. Its [x] buttons, data attributes and all, are
@@ -2226,9 +2267,9 @@ describe("removing a site in Settings disconnects it", () => {
return list;
}
// The hostnames a site list shows.
// The origins a site list shows.
function listed(list) {
return [...list.innerHTML.matchAll(/data-hostname="([^"]*)"/g)].map(
return [...list.innerHTML.matchAll(/data-origin="([^"]*)"/g)].map(
(match) => match[1],
);
}
@@ -2259,10 +2300,10 @@ describe("removing a site in Settings disconnects it", () => {
allowed: element("settings-allowed-sites"),
connected: element("settings-connected-sites"),
// Click the [x] beside a site, and let what it sends run.
remove: async (list, hostname) => {
expect(listed(list)).toContain(hostname);
remove: async (list, origin) => {
expect(listed(list)).toContain(origin);
const button = list.buttons.find(
(b) => b.dataset.hostname === hostname,
(b) => b.dataset.origin === origin,
);
await button.click();
await settle();
@@ -2274,14 +2315,15 @@ describe("removing a site in Settings disconnects it", () => {
delete global.document;
});
test("Settings lists a site connected without Remember", async () => {
test("Settings lists each site by its origin", async () => {
const bg = loadBackground({ actionPopup: true });
await connect(bg, FRESH_ORIGIN, false);
await connect(bg, FRESH_OTHER_PORT, true);
const settings = await openSettings(bg);
expect(listed(settings.connected)).toEqual(["fresh.example"]);
expect(listed(settings.allowed)).toEqual([HOSTNAME]);
expect(listed(settings.connected)).toEqual([FRESH_ORIGIN]);
expect(listed(settings.allowed)).toEqual([ORIGIN, FRESH_OTHER_PORT]);
});
test("removing a site connected without Remember disconnects it and tells its tabs", async () => {
@@ -2293,6 +2335,7 @@ describe("removing a site in Settings disconnects it", () => {
cb([
{ id: 1, url: FRESH_ORIGIN + "/app" },
{ id: 2, url: ORIGIN + "/app" },
{ id: 3, url: FRESH_OTHER_PORT + "/app" },
]),
sendMessage: (tabId, msg, cb) => {
sentToTabs.push({ tabId, msg });
@@ -2301,7 +2344,7 @@ describe("removing a site in Settings disconnects it", () => {
};
const settings = await openSettings(bg);
await settings.remove(settings.connected, "fresh.example");
await settings.remove(settings.connected, FRESH_ORIGIN);
expect(await siteAccounts(bg)).toEqual({ result: [] });
expect(sentToTabs).toEqual([
@@ -2321,20 +2364,45 @@ describe("removing a site in Settings disconnects it", () => {
});
});
// The same hostname can hold both kinds of connection: one origin allowed
// without Remember, then another, on a different port, allowed with it.
// One origin can hold both kinds of connection under two addresses:
// remembered for one, allowed without Remember for the other.
test("removing a remembered site also ends its connection made without Remember", async () => {
const bg = loadBackground({ actionPopup: true });
const stored = bg.storage.read("autistmask");
stored.wallets[0].addresses.push({
address: other.address,
balance: "0",
tokenBalances: [],
});
bg.storage.write("autistmask", stored);
await connect(bg, FRESH_ORIGIN, true);
bg.setActiveAddress(other.address);
const pending = bg.requestSite(FRESH_ORIGIN);
await settle();
bg.connectApproval(pending.id()).decide(true, false);
await settle();
expect(pending.result()).toEqual({ result: [other.address] });
const settings = await openSettings(bg);
await settings.remove(settings.allowed, FRESH_ORIGIN);
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({ result: [] });
});
test("removing a remembered site leaves the same host on another port connected", async () => {
const bg = loadBackground({ actionPopup: true });
await connect(bg, FRESH_ORIGIN, false);
await connect(bg, FRESH_OTHER_PORT, true);
const settings = await openSettings(bg);
await settings.remove(settings.allowed, "fresh.example");
await settings.remove(settings.allowed, FRESH_OTHER_PORT);
expect(await siteAccounts(bg, FRESH_OTHER_PORT)).toEqual({
result: [],
});
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({ result: [] });
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({
result: [signer.address],
});
});
test("a page can neither remove a site nor list the connected ones", async () => {
@@ -2343,7 +2411,7 @@ describe("removing a site in Settings disconnects it", () => {
const page = { url: FRESH_ORIGIN + "/index.html" };
const remove = bg.send(
{ type: "AUTISTMASK_REMOVE_SITE", hostname: "fresh.example" },
{ type: "AUTISTMASK_REMOVE_SITE", origin: FRESH_ORIGIN },
page,
);
const list = bg.send({ type: "AUTISTMASK_GET_CONNECTED_SITES" }, page);
+1 -2
View File
@@ -33,7 +33,6 @@ const signer = new Wallet(SIGNER_KEY);
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const CONNECTED_ORIGIN = "https://dapp.example";
const CONNECTED_HOSTNAME = "dapp.example";
const EXT_URL = "chrome-extension://autistmask/";
const MAINNET = networkById("mainnet");
@@ -81,7 +80,7 @@ function storedProfile(networkId) {
networkId,
rpcUrl: net.defaultRpcUrl,
blockscoutUrl: net.defaultBlockscoutUrl,
allowedSites: { [signer.address]: [CONNECTED_HOSTNAME] },
allowedSites: { [signer.address]: [CONNECTED_ORIGIN] },
deniedSites: {},
trackedTokens: [],
lastBalanceRefresh: 0,
+1 -2
View File
@@ -19,7 +19,6 @@ const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
// The site the persisted state has connected, and one it has never heard of.
const CONNECTED_ORIGIN = "https://dapp.example";
const CONNECTED_HOSTNAME = "dapp.example";
const STRANGER_ORIGIN = "https://stranger.example";
const MAINNET = networkById("mainnet");
@@ -86,7 +85,7 @@ function loadBackground() {
tokenHolderCache: {},
fraudContracts: [],
activeAddress: ADDRESS,
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
deniedSites: {},
};
const storage = makeStorageStub({ autistmask: persisted });
+1 -2
View File
@@ -17,7 +17,6 @@ const { networkById } = require("../src/shared/networks");
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const CONNECTED_ORIGIN = "https://dapp.example";
const CONNECTED_HOSTNAME = "dapp.example";
const UNKNOWN_ORIGIN = "https://stranger.example";
const MAINNET = networkById("mainnet");
@@ -41,7 +40,7 @@ function storedProfile(networkId) {
networkId,
rpcUrl: networkById(networkId).defaultRpcUrl,
blockscoutUrl: networkById(networkId).defaultBlockscoutUrl,
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
deniedSites: {},
trackedTokens: [],
};
+2 -3
View File
@@ -21,7 +21,6 @@ const { makeStorageStub } = require("./support/storageStub");
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const CONNECTED_ORIGIN = "https://dapp.example";
const CONNECTED_HOSTNAME = "dapp.example";
const MAINNET = networkById("mainnet");
const SEPOLIA = networkById("sepolia");
@@ -50,7 +49,7 @@ function storedProfile(networkId) {
networkId,
rpcUrl: CUSTOM_RPC,
blockscoutUrl: CUSTOM_BLOCKSCOUT,
allowedSites: { [ADDRESS]: [CONNECTED_HOSTNAME] },
allowedSites: { [ADDRESS]: [CONNECTED_ORIGIN] },
deniedSites: {},
trackedTokens: [{ address: TOKEN, symbol: "DAI", decimals: 18 }],
theme: "dark",
@@ -168,7 +167,7 @@ describe("a chain switch on a worker that never loaded state", () => {
expect(after.wallets).toEqual(walletFixture());
expect(after.hasWallet).toBe(true);
expect(after.activeAddress).toBe(ADDRESS);
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_HOSTNAME] });
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_ORIGIN] });
expect(after.trackedTokens).toEqual([
{ address: TOKEN, symbol: "DAI", decimals: 18 },
]);
+1 -2
View File
@@ -29,7 +29,6 @@ const signer = new Wallet(SIGNER_KEY);
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const CONNECTED_ORIGIN = "https://dapp.example";
const CONNECTED_HOSTNAME = "dapp.example";
const EXT_URL = "chrome-extension://autistmask/";
const SEPOLIA = networkById("sepolia");
@@ -67,7 +66,7 @@ function storedProfile(networkId) {
networkId,
rpcUrl: net.defaultRpcUrl,
blockscoutUrl: net.defaultBlockscoutUrl,
allowedSites: { [signer.address]: [CONNECTED_HOSTNAME] },
allowedSites: { [signer.address]: [CONNECTED_ORIGIN] },
deniedSites: {},
trackedTokens: [],
};
+1 -1
View File
@@ -150,7 +150,7 @@ async function openTxApproval(to, data) {
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
reply({
type: "tx",
hostname: "dapp.example",
origin: "https://dapp.example",
isPhishingDomain: false,
approvedFrom: FROM,
approvedTx: {
+9 -10
View File
@@ -438,11 +438,10 @@ step(
await d.switchToWindow(popup);
await d.waitVisible("#view-approve-site");
const hostname = await d.text("#approve-hostname");
const origin = await d.text("#approve-origin");
assert(
hostname === "127.0.0.1",
"the site prompt names the wrong origin: " +
JSON.stringify(hostname),
origin === env.server.origin,
"the site prompt names the wrong origin: " + JSON.stringify(origin),
);
const shown = await d.text("#approve-address");
assert(
@@ -494,16 +493,16 @@ step(
const screen = await d.execute(
`return {
hostname: document.getElementById("approve-sign-hostname").textContent,
origin: document.getElementById("approve-sign-origin").textContent,
type: document.getElementById("approve-sign-type").textContent,
message: document.getElementById("approve-sign-message").textContent,
from: document.getElementById("approve-sign-from").textContent,
};`,
);
assert(
screen.hostname === "127.0.0.1",
screen.origin === env.server.origin,
"the sign prompt names the wrong origin: " +
JSON.stringify(screen.hostname),
JSON.stringify(screen.origin),
);
assert(
screen.type === "Personal message",
@@ -571,7 +570,7 @@ step(
const screen = await d.execute(
`return {
hostname: document.getElementById("approve-tx-hostname").textContent,
origin: document.getElementById("approve-tx-origin").textContent,
from: document.getElementById("approve-tx-from").textContent,
to: document.getElementById("approve-tx-to").textContent,
value: document.getElementById("approve-tx-value").textContent,
@@ -582,9 +581,9 @@ step(
};`,
);
assert(
screen.hostname === "127.0.0.1",
screen.origin === env.server.origin,
"the transaction prompt names the wrong origin: " +
JSON.stringify(screen.hostname),
JSON.stringify(screen.origin),
);
assert(
screen.from.toLowerCase().includes(env.address.toLowerCase()),
+16 -19
View File
@@ -35,6 +35,7 @@ const {
const {
DAPP_ORIGIN,
DAPP_URL,
PHISHING_DAPP_ORIGIN,
PHISHING_DAPP_URL,
FEE_ESTIMATE_WEI,
FEE_RESERVE_WEI,
@@ -2471,8 +2472,6 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
// dApp, with real funds, against a real network. The RPC is stubbed
// throughout. That pass stays on the human list before 1.0.0.
const DAPP_HOSTNAME = new URL(DAPP_URL).hostname;
// The personal_sign payload. Sent as hex, which is what dApps send and what
// the popup requires — it calls getBytes() on the message — and displayed on
// the approval screen as the decoded text, which is what the user is agreeing
@@ -3016,11 +3015,10 @@ test("eth_requestAccounts rejected at the prompt returns a rejection (#183)", as
try {
await visible(popup, "#view-approve-site");
const hostname = await popup.locator("#approve-hostname").innerText();
const origin = await popup.locator("#approve-origin").innerText();
assert(
hostname === DAPP_HOSTNAME,
"the site prompt names the wrong origin: " +
JSON.stringify(hostname),
origin === DAPP_ORIGIN,
"the site prompt names the wrong origin: " + JSON.stringify(origin),
);
// The control for the phishing test below: this origin is not on the
@@ -3101,7 +3099,6 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
// check and the real approval screen. Nothing about the list is stubbed —
// there is nothing left to stub, since the extension no longer fetches it.
const phishingDapp = await openDapp(env.ctx, PHISHING_DAPP_URL);
const hostname = new URL(PHISHING_DAPP_URL).hostname;
try {
await reserveApprovalTab(env);
await startRequest(
@@ -3114,15 +3111,15 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
try {
await visible(popup, "#view-approve-site");
const shown = await popup.locator("#approve-hostname").innerText();
const shown = await popup.locator("#approve-origin").innerText();
assert(
shown === hostname,
shown === PHISHING_DAPP_ORIGIN,
"the site prompt names the wrong origin: " +
JSON.stringify(shown),
);
await visible(popup, "#approve-site-phishing-warning");
console.log("# phishing warning shown for " + hostname);
console.log("# phishing warning shown for " + PHISHING_DAPP_ORIGIN);
// Not remembered: a remembered decision for this origin would
// outlive the test.
@@ -3152,15 +3149,15 @@ test("personal_sign signs, and the signature recovers to the address (#183)", as
const boundary = await watchApprovalBoundary(popup, env);
const screen = await popup.evaluate(() => ({
hostname: document.getElementById("approve-sign-hostname").textContent,
origin: document.getElementById("approve-sign-origin").textContent,
type: document.getElementById("approve-sign-type").textContent,
message: document.getElementById("approve-sign-message").textContent,
from: document.getElementById("approve-sign-from").textContent,
}));
assert(
screen.hostname === DAPP_HOSTNAME,
screen.origin === DAPP_ORIGIN,
"the sign prompt names the wrong origin: " +
JSON.stringify(screen.hostname),
JSON.stringify(screen.origin),
);
assert(
screen.type === "Personal message",
@@ -3245,15 +3242,15 @@ test("eth_signTypedData_v4 signs, and the signature recovers (#183)", async (env
const boundary = await watchApprovalBoundary(popup, env);
const screen = await popup.evaluate(() => ({
hostname: document.getElementById("approve-sign-hostname").textContent,
origin: document.getElementById("approve-sign-origin").textContent,
type: document.getElementById("approve-sign-type").textContent,
message: document.getElementById("approve-sign-message").innerText,
from: document.getElementById("approve-sign-from").textContent,
}));
assert(
screen.hostname === DAPP_HOSTNAME,
screen.origin === DAPP_ORIGIN,
"the typed data prompt names the wrong origin: " +
JSON.stringify(screen.hostname),
JSON.stringify(screen.origin),
);
assert(
screen.type === "Typed data (EIP-712)",
@@ -3351,7 +3348,7 @@ test("eth_sendTransaction signs the approved transaction and broadcasts it (#183
const boundary = await watchApprovalBoundary(popup, env);
const screen = await popup.evaluate(() => ({
hostname: document.getElementById("approve-tx-hostname").textContent,
origin: document.getElementById("approve-tx-origin").textContent,
from: document.getElementById("approve-tx-from").textContent,
to: document.getElementById("approve-tx-to").textContent,
value: document.getElementById("approve-tx-value").textContent,
@@ -3361,9 +3358,9 @@ test("eth_sendTransaction signs the approved transaction and broadcasts it (#183
.classList.contains("hidden"),
}));
assert(
screen.hostname === DAPP_HOSTNAME,
screen.origin === DAPP_ORIGIN,
"the transaction prompt names the wrong origin: " +
JSON.stringify(screen.hostname),
JSON.stringify(screen.origin),
);
assert(
screen.from.toLowerCase().includes(env.expectedAddress.toLowerCase()),
+1 -1
View File
@@ -232,7 +232,7 @@ async function confirmSend(amount, token = "ETH") {
async function approveTxWithFeePerGas(maxFeePerGas) {
approvalDetails = {
type: "tx",
hostname: "dapp.example",
origin: "https://dapp.example",
approvedFrom: HOLDER,
approvedTx: {
to: RECIPIENT,
+1 -1
View File
@@ -471,7 +471,7 @@ async function openSignScreen(data) {
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
reply({
type: "sign",
hostname: "dapp.example",
origin: "https://dapp.example",
isPhishingDomain: false,
approvedFrom: OWNER,
signParams: request(data),