Compare commits
2 Commits
chore/toke
...
ab3b452924
| Author | SHA1 | Date | |
|---|---|---|---|
| ab3b452924 | |||
| 23aeae4841 |
@@ -1,4 +1,3 @@
|
|||||||
.git
|
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
|
|||||||
4
TODO.md
4
TODO.md
@@ -23,6 +23,10 @@ get review, merge to main.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-09: dApp approval signing moved into the popup — the password no
|
||||||
|
longer crosses the extension messaging boundary; the background broadcasts and
|
||||||
|
resolves approvals only, and verifies the signed artifact against the approval
|
||||||
|
it holds (#157).
|
||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
||||||
shims, README Entrypoints section
|
shims, README Entrypoints section
|
||||||
- 2026-03-01: About well in settings with build info and easter egg (in flight
|
- 2026-03-01: About well in settings with build info and easter egg (in flight
|
||||||
|
|||||||
46
build.js
46
build.js
@@ -11,9 +11,51 @@ function ensureDir(dir) {
|
|||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBuildInfo() {
|
||||||
|
const pkg = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
||||||
|
);
|
||||||
|
let commitHash = "unknown";
|
||||||
|
try {
|
||||||
|
commitHash = execSync("git rev-parse --short HEAD", {
|
||||||
|
encoding: "utf8",
|
||||||
|
}).trim();
|
||||||
|
} catch (_) {
|
||||||
|
// not a git repo or git not available
|
||||||
|
}
|
||||||
|
let commitHashFull = "unknown";
|
||||||
|
try {
|
||||||
|
commitHashFull = execSync("git rev-parse HEAD", {
|
||||||
|
encoding: "utf8",
|
||||||
|
}).trim();
|
||||||
|
} catch (_) {
|
||||||
|
// not a git repo or git not available
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: pkg.version,
|
||||||
|
license: pkg.license,
|
||||||
|
author: pkg.author,
|
||||||
|
commitHash,
|
||||||
|
commitHashFull,
|
||||||
|
buildDate: new Date().toISOString().slice(0, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
console.log("Building AutistMask extension...");
|
console.log("Building AutistMask extension...");
|
||||||
|
|
||||||
|
const buildInfo = getBuildInfo();
|
||||||
|
console.log("Build info:", buildInfo);
|
||||||
|
|
||||||
|
const define = {
|
||||||
|
__BUILD_VERSION__: JSON.stringify(buildInfo.version),
|
||||||
|
__BUILD_LICENSE__: JSON.stringify(buildInfo.license),
|
||||||
|
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author),
|
||||||
|
__BUILD_COMMIT__: JSON.stringify(buildInfo.commitHash),
|
||||||
|
__BUILD_COMMIT_FULL__: JSON.stringify(buildInfo.commitHashFull),
|
||||||
|
__BUILD_DATE__: JSON.stringify(buildInfo.buildDate),
|
||||||
|
};
|
||||||
|
|
||||||
// compile tailwind CSS
|
// compile tailwind CSS
|
||||||
console.log("Compiling Tailwind CSS...");
|
console.log("Compiling Tailwind CSS...");
|
||||||
const tailwindInput = path.join(SRC, "popup", "styles", "main.css");
|
const tailwindInput = path.join(SRC, "popup", "styles", "main.css");
|
||||||
@@ -38,6 +80,7 @@ async function build() {
|
|||||||
platform: "browser",
|
platform: "browser",
|
||||||
target: ["chrome110", "firefox110"],
|
target: ["chrome110", "firefox110"],
|
||||||
minify: true,
|
minify: true,
|
||||||
|
define,
|
||||||
});
|
});
|
||||||
|
|
||||||
// bundle background script
|
// bundle background script
|
||||||
@@ -49,6 +92,7 @@ async function build() {
|
|||||||
platform: "browser",
|
platform: "browser",
|
||||||
target: ["chrome110", "firefox110"],
|
target: ["chrome110", "firefox110"],
|
||||||
minify: true,
|
minify: true,
|
||||||
|
define,
|
||||||
});
|
});
|
||||||
|
|
||||||
// bundle content script
|
// bundle content script
|
||||||
@@ -60,6 +104,7 @@ async function build() {
|
|||||||
platform: "browser",
|
platform: "browser",
|
||||||
target: ["chrome110", "firefox110"],
|
target: ["chrome110", "firefox110"],
|
||||||
minify: true,
|
minify: true,
|
||||||
|
define,
|
||||||
});
|
});
|
||||||
|
|
||||||
// bundle inpage script (injected into page context, separate file)
|
// bundle inpage script (injected into page context, separate file)
|
||||||
@@ -71,6 +116,7 @@ async function build() {
|
|||||||
platform: "browser",
|
platform: "browser",
|
||||||
target: ["chrome110", "firefox110"],
|
target: ["chrome110", "firefox110"],
|
||||||
minify: true,
|
minify: true,
|
||||||
|
define,
|
||||||
});
|
});
|
||||||
|
|
||||||
// copy popup HTML
|
// copy popup HTML
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
const { DEFAULT_RPC_URL } = require("../shared/constants");
|
const { DEFAULT_RPC_URL } = require("../shared/constants");
|
||||||
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
|
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
|
||||||
const { onChainSwitch } = require("../shared/chainSwitch");
|
const { onChainSwitch } = require("../shared/chainSwitch");
|
||||||
const { getBytes } = require("ethers");
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
loadState,
|
loadState,
|
||||||
@@ -14,8 +13,7 @@ const {
|
|||||||
} = require("../shared/state");
|
} = 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");
|
||||||
const { decryptWithPassword } = require("../shared/vault");
|
const { verifySignedTx, verifySignature } = require("../shared/approvalVerify");
|
||||||
const { getSignerForAddress } = require("../shared/wallet");
|
|
||||||
const {
|
const {
|
||||||
isPhishingDomain,
|
isPhishingDomain,
|
||||||
updatePhishingList,
|
updatePhishingList,
|
||||||
@@ -725,39 +723,28 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The popup signs; it reports back here when it could not. Fail the
|
||||||
|
// request the same way this handler used to when it did the signing.
|
||||||
|
if (msg.error) {
|
||||||
|
approval.resolve({ error: { message: msg.error } });
|
||||||
|
sendResponse({ error: msg.error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await loadState();
|
await loadState();
|
||||||
const activeAddress = await getActiveAddress();
|
const activeAddress = await getActiveAddress();
|
||||||
let wallet, addrIndex;
|
// The popup holds the secret, but the background stays the
|
||||||
for (const w of state.wallets) {
|
// authority on what is broadcast: the raw transaction must be
|
||||||
for (let i = 0; i < w.addresses.length; i++) {
|
// the approved one, signed by the approved address.
|
||||||
if (w.addresses[i].address === activeAddress) {
|
verifySignedTx(
|
||||||
wallet = w;
|
msg.rawSignedTx,
|
||||||
addrIndex = i;
|
approval.txParams,
|
||||||
break;
|
activeAddress,
|
||||||
}
|
|
||||||
}
|
|
||||||
if (wallet) break;
|
|
||||||
}
|
|
||||||
if (!wallet) throw new Error("Wallet not found");
|
|
||||||
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
|
||||||
let decrypted = await decryptWithPassword(
|
|
||||||
wallet.encryptedSecret,
|
|
||||||
msg.password,
|
|
||||||
);
|
);
|
||||||
const signer = getSignerForAddress(
|
|
||||||
wallet,
|
|
||||||
addrIndex,
|
|
||||||
decrypted,
|
|
||||||
);
|
|
||||||
// Best-effort: clear decrypted secret after use.
|
|
||||||
// Note: JS strings are immutable; this nulls the reference but
|
|
||||||
// the original string may persist in memory until GC.
|
|
||||||
decrypted = null;
|
|
||||||
const provider = getProvider(state.rpcUrl);
|
const provider = getProvider(state.rpcUrl);
|
||||||
const connected = signer.connect(provider);
|
const tx = await provider.broadcastTransaction(msg.rawSignedTx);
|
||||||
const tx = await connected.sendTransaction(approval.txParams);
|
|
||||||
approval.resolve({ txHash: tx.hash });
|
approval.resolve({ txHash: tx.hash });
|
||||||
sendResponse({ txHash: tx.hash });
|
sendResponse({ txHash: tx.hash });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -784,55 +771,23 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The popup signs; it reports back here when it could not. Fail the
|
||||||
|
// request the same way this handler used to when it did the signing.
|
||||||
|
if (msg.error) {
|
||||||
|
approval.resolve({ error: { message: msg.error } });
|
||||||
|
sendResponse({ error: msg.error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await loadState();
|
|
||||||
const activeAddress = await getActiveAddress();
|
const activeAddress = await getActiveAddress();
|
||||||
let wallet, addrIndex;
|
// The popup holds the secret, but the background stays the
|
||||||
for (const w of state.wallets) {
|
// authority on what is handed back to the page: the signature
|
||||||
for (let i = 0; i < w.addresses.length; i++) {
|
// must cover the approved payload and recover to the approved
|
||||||
if (w.addresses[i].address === activeAddress) {
|
// address.
|
||||||
wallet = w;
|
const signature = msg.signature;
|
||||||
addrIndex = i;
|
verifySignature(approval.signParams, signature, activeAddress);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (wallet) break;
|
|
||||||
}
|
|
||||||
if (!wallet) throw new Error("Wallet not found");
|
|
||||||
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
|
||||||
let decrypted = await decryptWithPassword(
|
|
||||||
wallet.encryptedSecret,
|
|
||||||
msg.password,
|
|
||||||
);
|
|
||||||
const signer = getSignerForAddress(
|
|
||||||
wallet,
|
|
||||||
addrIndex,
|
|
||||||
decrypted,
|
|
||||||
);
|
|
||||||
// Best-effort: clear decrypted secret after use.
|
|
||||||
// Note: JS strings are immutable; this nulls the reference but
|
|
||||||
// the original string may persist in memory until GC.
|
|
||||||
decrypted = null;
|
|
||||||
|
|
||||||
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 });
|
approval.resolve({ signature });
|
||||||
sendResponse({ signature });
|
sendResponse({ signature });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1002,6 +1002,64 @@
|
|||||||
</p>
|
</p>
|
||||||
<div id="settings-denied-sites"></div>
|
<div id="settings-denied-sites"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
<h3 class="font-bold mb-1">About</h3>
|
||||||
|
<p class="text-xs mb-2">
|
||||||
|
<a
|
||||||
|
href="https://git.eeqj.de/sneak/AutistMask"
|
||||||
|
class="underline decoration-dashed"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>AutistMask</a
|
||||||
|
>
|
||||||
|
— Minimal Ethereum wallet browser extension.
|
||||||
|
</p>
|
||||||
|
<div class="text-xs">
|
||||||
|
<div class="mb-1">
|
||||||
|
<span class="text-muted">License:</span>
|
||||||
|
<span id="about-license"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<span class="text-muted">Author:</span>
|
||||||
|
<span id="about-author"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<span class="text-muted">Version:</span>
|
||||||
|
<span
|
||||||
|
id="about-version"
|
||||||
|
class="cursor-pointer select-none"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<span class="text-muted">Release date:</span>
|
||||||
|
<span id="about-release-date"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted">Commit:</span>
|
||||||
|
<a
|
||||||
|
id="about-commit-link"
|
||||||
|
class="underline decoration-dashed"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="settings-debug-well"
|
||||||
|
class="bg-well p-3 mx-1 mb-3"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<h3 class="font-bold mb-1">Debug</h3>
|
||||||
|
<label
|
||||||
|
class="text-xs flex items-center gap-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
<input type="checkbox" id="settings-debug-mode" />
|
||||||
|
Enable debug mode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============ DELETE WALLET CONFIRM ============ -->
|
<!-- ============ DELETE WALLET CONFIRM ============ -->
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
// AutistMask popup entry point.
|
// AutistMask popup entry point.
|
||||||
// Loads state, initializes views, triggers first render.
|
// Loads state, initializes views, triggers first render.
|
||||||
|
|
||||||
const { DEBUG } = require("../shared/constants");
|
const { state, saveState, loadState } = require("../shared/state");
|
||||||
const {
|
const { setRuntimeDebug } = require("../shared/log");
|
||||||
state,
|
|
||||||
saveState,
|
|
||||||
loadState,
|
|
||||||
currentNetwork,
|
|
||||||
} = require("../shared/state");
|
|
||||||
const { refreshPrices } = require("../shared/prices");
|
const { refreshPrices } = require("../shared/prices");
|
||||||
const { refreshBalances } = require("../shared/balances");
|
const { refreshBalances } = require("../shared/balances");
|
||||||
const {
|
const {
|
||||||
$,
|
$,
|
||||||
showView,
|
showView,
|
||||||
|
updateDebugBanner,
|
||||||
setRenderMain,
|
setRenderMain,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
goBack,
|
goBack,
|
||||||
@@ -209,21 +205,11 @@ async function init() {
|
|||||||
await loadState();
|
await loadState();
|
||||||
applyTheme(state.theme);
|
applyTheme(state.theme);
|
||||||
|
|
||||||
const net = currentNetwork();
|
// Sync runtime debug flag from persisted state before first render
|
||||||
if (DEBUG || net.isTestnet) {
|
setRuntimeDebug(state.debugMode);
|
||||||
const banner = document.createElement("div");
|
|
||||||
banner.id = "debug-banner";
|
// Create the debug/testnet banner if needed (uses runtime debug state)
|
||||||
if (DEBUG && net.isTestnet) {
|
updateDebugBanner();
|
||||||
banner.textContent = "DEBUG / INSECURE [TESTNET]";
|
|
||||||
} else if (net.isTestnet) {
|
|
||||||
banner.textContent = "[TESTNET]";
|
|
||||||
} else {
|
|
||||||
banner.textContent = "DEBUG / INSECURE";
|
|
||||||
}
|
|
||||||
banner.style.cssText =
|
|
||||||
"background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;";
|
|
||||||
document.body.prepend(banner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-default active address
|
// Auto-default active address
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
--color-border: #000000;
|
--color-border: #000000;
|
||||||
--color-border-light: #cccccc;
|
--color-border-light: #cccccc;
|
||||||
--color-hover: #eeeeee;
|
--color-hover: #eeeeee;
|
||||||
--color-well: #f5f5f5;
|
--color-well: #e8e8e8;
|
||||||
--color-danger-well: #fef2f2;
|
--color-danger-well: #fef2f2;
|
||||||
--color-section: #dddddd;
|
--color-section: #dddddd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,19 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
|
const {
|
||||||
|
formatEther,
|
||||||
|
formatUnits,
|
||||||
|
getBytes,
|
||||||
|
Interface,
|
||||||
|
toUtf8String,
|
||||||
|
} = require("ethers");
|
||||||
const { getPrice, formatUsd } = require("../../shared/prices");
|
const { getPrice, formatUsd } = require("../../shared/prices");
|
||||||
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 { decryptWithPassword } = require("../../shared/vault");
|
||||||
|
const { getSignerForAddress } = require("../../shared/wallet");
|
||||||
|
const { getProvider } = require("../../shared/balances");
|
||||||
const txStatus = require("./txStatus");
|
const txStatus = require("./txStatus");
|
||||||
const uniswap = require("../../shared/uniswap");
|
const uniswap = require("../../shared/uniswap");
|
||||||
const runtime =
|
const runtime =
|
||||||
@@ -153,6 +162,8 @@ function showTxApproval(details) {
|
|||||||
details.isPhishingDomain,
|
details.isPhishingDomain,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
pendingTxParams = details.txParams;
|
||||||
|
|
||||||
const toAddr = details.txParams.to;
|
const toAddr = details.txParams.to;
|
||||||
const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
|
const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
|
||||||
const ethValue = formatEther(details.txParams.value || "0");
|
const ethValue = formatEther(details.txParams.value || "0");
|
||||||
@@ -326,6 +337,7 @@ function showSignApproval(details) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sp = details.signParams;
|
const sp = details.signParams;
|
||||||
|
pendingSignParams = sp;
|
||||||
|
|
||||||
$("approve-sign-hostname").textContent = details.hostname;
|
$("approve-sign-hostname").textContent = details.hostname;
|
||||||
$("approve-sign-from").innerHTML = approvalAddressHtml(sp.from);
|
$("approve-sign-from").innerHTML = approvalAddressHtml(sp.from);
|
||||||
@@ -401,6 +413,36 @@ function show(id) {
|
|||||||
|
|
||||||
let approvalId = null;
|
let approvalId = null;
|
||||||
let pendingTxDetails = null;
|
let pendingTxDetails = null;
|
||||||
|
// The exact parameters shown to the user, kept so the popup signs what it
|
||||||
|
// displayed rather than re-fetching anything at approval time. Both are
|
||||||
|
// repopulated by show() when the popup is closed and reopened.
|
||||||
|
let pendingTxParams = null;
|
||||||
|
let pendingSignParams = null;
|
||||||
|
|
||||||
|
// Approve buttons stay disabled and muted while the popup derives the key and
|
||||||
|
// signs, which is slow enough (Argon2id) that a double click is likely.
|
||||||
|
function setTxButtonBusy(busy) {
|
||||||
|
$("btn-approve-tx").disabled = busy;
|
||||||
|
$("btn-approve-tx").classList.toggle("text-muted", busy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSignButtonBusy(busy) {
|
||||||
|
$("btn-approve-sign").disabled = busy;
|
||||||
|
$("btn-approve-sign").classList.toggle("text-muted", busy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate the wallet and the address index owning the currently active
|
||||||
|
// address. Returns null when no wallet holds it.
|
||||||
|
function findActiveWallet() {
|
||||||
|
for (const wallet of state.wallets) {
|
||||||
|
for (let i = 0; i < wallet.addresses.length; i++) {
|
||||||
|
if (wallet.addresses[i].address === state.activeAddress) {
|
||||||
|
return { wallet, addrIndex: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function init(ctx) {
|
function init(ctx) {
|
||||||
$("approve-remember").addEventListener("change", async () => {
|
$("approve-remember").addEventListener("change", async () => {
|
||||||
@@ -430,25 +472,78 @@ function init(ctx) {
|
|||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-approve-tx").addEventListener("click", () => {
|
$("btn-approve-tx").addEventListener("click", async () => {
|
||||||
const password = $("approve-tx-password").value;
|
let password = $("approve-tx-password").value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
showError("approve-tx-error", "Please enter your password.");
|
showError("approve-tx-error", "Please enter your password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideError("approve-tx-error");
|
hideError("approve-tx-error");
|
||||||
$("btn-approve-tx").disabled = true;
|
setTxButtonBusy(true);
|
||||||
$("btn-approve-tx").classList.add("text-muted");
|
|
||||||
|
|
||||||
runtime.sendMessage(
|
const active = findActiveWallet();
|
||||||
{
|
if (!active) {
|
||||||
|
password = null;
|
||||||
|
showError(
|
||||||
|
"approve-tx-error",
|
||||||
|
"No wallet was found for the active address.",
|
||||||
|
);
|
||||||
|
setTxButtonBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt here, in the popup. The password must never cross the
|
||||||
|
// extension messaging boundary; only the signed transaction does.
|
||||||
|
let decryptedSecret;
|
||||||
|
try {
|
||||||
|
decryptedSecret = await decryptWithPassword(
|
||||||
|
active.wallet.encryptedSecret,
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
showError(
|
||||||
|
"approve-tx-error",
|
||||||
|
"That password is incorrect. Please try again.",
|
||||||
|
);
|
||||||
|
setTxButtonBusy(false);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
// Best-effort: drop the password as soon as the key derivation
|
||||||
|
// is done. Note that JS strings are immutable; this clears the
|
||||||
|
// reference but the original string may persist until GC.
|
||||||
|
password = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
type: "AUTISTMASK_TX_RESPONSE",
|
type: "AUTISTMASK_TX_RESPONSE",
|
||||||
id: approvalId,
|
id: approvalId,
|
||||||
approved: true,
|
approved: true,
|
||||||
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
};
|
||||||
password: password,
|
try {
|
||||||
},
|
const signer = getSignerForAddress(
|
||||||
(response) => {
|
active.wallet,
|
||||||
|
active.addrIndex,
|
||||||
|
decryptedSecret,
|
||||||
|
);
|
||||||
|
const provider = getProvider(state.rpcUrl);
|
||||||
|
const connected = signer.connect(provider);
|
||||||
|
// This is the sequence ethers' own sendTransaction() runs
|
||||||
|
// internally, so nonce, gas, fee and chain id population are
|
||||||
|
// identical to when the background did the signing.
|
||||||
|
const populated =
|
||||||
|
await connected.populateTransaction(pendingTxParams);
|
||||||
|
delete populated.from;
|
||||||
|
payload.rawSignedTx = await connected.signTransaction(populated);
|
||||||
|
} catch (e) {
|
||||||
|
payload.error =
|
||||||
|
e.shortMessage || e.message || "Transaction signing failed.";
|
||||||
|
} finally {
|
||||||
|
// Best-effort: clear the decrypted secret after use, with the
|
||||||
|
// same immutability caveat as the password above.
|
||||||
|
decryptedSecret = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.sendMessage(payload, (response) => {
|
||||||
if (response && response.txHash) {
|
if (response && response.txHash) {
|
||||||
txStatus.showWait(pendingTxDetails, response.txHash);
|
txStatus.showWait(pendingTxDetails, response.txHash);
|
||||||
} else {
|
} else {
|
||||||
@@ -456,8 +551,7 @@ function init(ctx) {
|
|||||||
(response && response.error) || "Transaction failed.";
|
(response && response.error) || "Transaction failed.";
|
||||||
txStatus.showError(pendingTxDetails, null, msg);
|
txStatus.showError(pendingTxDetails, null, msg);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-reject-tx").addEventListener("click", () => {
|
$("btn-reject-tx").addEventListener("click", () => {
|
||||||
@@ -469,36 +563,93 @@ function init(ctx) {
|
|||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-approve-sign").addEventListener("click", () => {
|
$("btn-approve-sign").addEventListener("click", async () => {
|
||||||
const password = $("approve-sign-password").value;
|
let password = $("approve-sign-password").value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
showError("approve-sign-error", "Please enter your password.");
|
showError("approve-sign-error", "Please enter your password.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideError("approve-sign-error");
|
hideError("approve-sign-error");
|
||||||
$("btn-approve-sign").disabled = true;
|
setSignButtonBusy(true);
|
||||||
$("btn-approve-sign").classList.add("text-muted");
|
|
||||||
|
|
||||||
runtime.sendMessage(
|
const active = findActiveWallet();
|
||||||
{
|
if (!active) {
|
||||||
|
password = null;
|
||||||
|
showError(
|
||||||
|
"approve-sign-error",
|
||||||
|
"No wallet was found for the active address.",
|
||||||
|
);
|
||||||
|
setSignButtonBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt here, in the popup. The password must never cross the
|
||||||
|
// extension messaging boundary; only the signature does.
|
||||||
|
let decryptedSecret;
|
||||||
|
try {
|
||||||
|
decryptedSecret = await decryptWithPassword(
|
||||||
|
active.wallet.encryptedSecret,
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
showError(
|
||||||
|
"approve-sign-error",
|
||||||
|
"That password is incorrect. Please try again.",
|
||||||
|
);
|
||||||
|
setSignButtonBusy(false);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
// Best-effort: drop the password as soon as the key derivation
|
||||||
|
// is done. Note that JS strings are immutable; this clears the
|
||||||
|
// reference but the original string may persist until GC.
|
||||||
|
password = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
type: "AUTISTMASK_SIGN_RESPONSE",
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
||||||
id: approvalId,
|
id: approvalId,
|
||||||
approved: true,
|
approved: true,
|
||||||
// TODO(security): Move decryption to popup to avoid sending password via runtime.sendMessage
|
};
|
||||||
password: password,
|
try {
|
||||||
},
|
const signer = getSignerForAddress(
|
||||||
(response) => {
|
active.wallet,
|
||||||
|
active.addrIndex,
|
||||||
|
decryptedSecret,
|
||||||
|
);
|
||||||
|
const sp = pendingSignParams;
|
||||||
|
if (sp.method === "personal_sign" || sp.method === "eth_sign") {
|
||||||
|
payload.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;
|
||||||
|
payload.signature = await signer.signTypedData(
|
||||||
|
domain,
|
||||||
|
types,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
payload.error = e.shortMessage || e.message || "Signing failed.";
|
||||||
|
} finally {
|
||||||
|
// Best-effort: clear the decrypted secret after use, with the
|
||||||
|
// same immutability caveat as the password above.
|
||||||
|
decryptedSecret = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.sendMessage(payload, (response) => {
|
||||||
if (response && response.signature) {
|
if (response && response.signature) {
|
||||||
window.close();
|
window.close();
|
||||||
} else {
|
} else {
|
||||||
const msg =
|
const msg = (response && response.error) || "Signing failed.";
|
||||||
(response && response.error) || "Signing failed.";
|
|
||||||
showError("approve-sign-error", msg);
|
showError("approve-sign-error", msg);
|
||||||
$("btn-approve-sign").disabled = false;
|
setSignButtonBusy(false);
|
||||||
$("btn-approve-sign").classList.remove("text-muted");
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-reject-sign").addEventListener("click", () => {
|
$("btn-reject-sign").addEventListener("click", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Shared DOM helpers used by all views.
|
// Shared DOM helpers used by all views.
|
||||||
|
|
||||||
const { DEBUG } = require("../../shared/constants");
|
const { isDebug } = require("../../shared/log");
|
||||||
const {
|
const {
|
||||||
formatUsd,
|
formatUsd,
|
||||||
getPrice,
|
getPrice,
|
||||||
@@ -59,19 +59,37 @@ function showView(name) {
|
|||||||
clearFlash();
|
clearFlash();
|
||||||
state.currentView = name;
|
state.currentView = name;
|
||||||
saveState();
|
saveState();
|
||||||
|
updateDebugBanner(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the debug/insecure warning banner.
|
||||||
|
// Called on every view switch and after the settings debug toggle changes.
|
||||||
|
// The banner is shown when the compile-time DEBUG constant is true OR when
|
||||||
|
// the user has enabled runtime debug mode via the settings easter egg, OR
|
||||||
|
// when the active network is a testnet.
|
||||||
|
function updateDebugBanner(viewName) {
|
||||||
|
const debug = isDebug();
|
||||||
const net = currentNetwork();
|
const net = currentNetwork();
|
||||||
if (DEBUG || net.isTestnet) {
|
const show = debug || net.isTestnet;
|
||||||
const banner = document.getElementById("debug-banner");
|
let banner = document.getElementById("debug-banner");
|
||||||
if (banner) {
|
if (show) {
|
||||||
if (DEBUG && net.isTestnet) {
|
if (!banner) {
|
||||||
banner.textContent =
|
banner = document.createElement("div");
|
||||||
"DEBUG / INSECURE [TESTNET] (" + name + ")";
|
banner.id = "debug-banner";
|
||||||
|
banner.style.cssText =
|
||||||
|
"background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;";
|
||||||
|
document.body.prepend(banner);
|
||||||
|
}
|
||||||
|
const suffix = viewName ? " (" + viewName + ")" : "";
|
||||||
|
if (debug && net.isTestnet) {
|
||||||
|
banner.textContent = "DEBUG / INSECURE [TESTNET]" + suffix;
|
||||||
} else if (net.isTestnet) {
|
} else if (net.isTestnet) {
|
||||||
banner.textContent = "[TESTNET]";
|
banner.textContent = "[TESTNET]" + suffix;
|
||||||
} else {
|
} else {
|
||||||
banner.textContent = "DEBUG / INSECURE (" + name + ")";
|
banner.textContent = "DEBUG / INSECURE" + suffix;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (banner) {
|
||||||
|
banner.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +435,7 @@ module.exports = {
|
|||||||
showError,
|
showError,
|
||||||
hideError,
|
hideError,
|
||||||
showView,
|
showView,
|
||||||
|
updateDebugBanner,
|
||||||
setRenderMain,
|
setRenderMain,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
goBack,
|
goBack,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const {
|
const {
|
||||||
$,
|
$,
|
||||||
showView,
|
showView,
|
||||||
|
updateDebugBanner,
|
||||||
showFlash,
|
showFlash,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
flashCopyFeedback,
|
||||||
goBack,
|
goBack,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
@@ -10,12 +12,23 @@ const { applyTheme } = require("../theme");
|
|||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
|
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
|
||||||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch, setRuntimeDebug } = require("../../shared/log");
|
||||||
const deleteWallet = require("./deleteWallet");
|
const deleteWallet = require("./deleteWallet");
|
||||||
|
const {
|
||||||
|
BUILD_VERSION,
|
||||||
|
BUILD_LICENSE,
|
||||||
|
BUILD_AUTHOR,
|
||||||
|
BUILD_COMMIT,
|
||||||
|
BUILD_DATE,
|
||||||
|
GITEA_COMMIT_URL,
|
||||||
|
} = require("../../shared/buildInfo");
|
||||||
|
|
||||||
const runtime =
|
const runtime =
|
||||||
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
|
|
||||||
|
let versionClickCount = 0;
|
||||||
|
let versionClickTimer = null;
|
||||||
|
|
||||||
function renderSiteList(containerId, siteMap, stateKey) {
|
function renderSiteList(containerId, siteMap, stateKey) {
|
||||||
const container = $(containerId);
|
const container = $(containerId);
|
||||||
const hostnames = [...new Set(Object.values(siteMap).flat())];
|
const hostnames = [...new Set(Object.values(siteMap).flat())];
|
||||||
@@ -142,6 +155,28 @@ function show() {
|
|||||||
renderSiteLists();
|
renderSiteLists();
|
||||||
renderWalletListSettings();
|
renderWalletListSettings();
|
||||||
|
|
||||||
|
// Populate About well
|
||||||
|
$("about-license").textContent = BUILD_LICENSE;
|
||||||
|
// Show only the name part of the author field (strip email)
|
||||||
|
const authorName = BUILD_AUTHOR.replace(/\s*<[^>]+>/, "");
|
||||||
|
$("about-author").textContent = authorName;
|
||||||
|
$("about-version").textContent = BUILD_VERSION;
|
||||||
|
$("about-release-date").textContent = BUILD_DATE;
|
||||||
|
$("about-commit-link").textContent = BUILD_COMMIT;
|
||||||
|
$("about-commit-link").href = GITEA_COMMIT_URL;
|
||||||
|
|
||||||
|
// Reset version click counter each time settings opens
|
||||||
|
versionClickCount = 0;
|
||||||
|
|
||||||
|
// Show debug well if debug mode is already enabled
|
||||||
|
const debugWell = $("settings-debug-well");
|
||||||
|
if (state.debugMode) {
|
||||||
|
debugWell.style.display = "";
|
||||||
|
} else {
|
||||||
|
debugWell.style.display = "none";
|
||||||
|
}
|
||||||
|
$("settings-debug-mode").checked = state.debugMode;
|
||||||
|
|
||||||
showView("settings");
|
showView("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +324,66 @@ function init(ctx) {
|
|||||||
ctx.showSettingsAddTokenView,
|
ctx.showSettingsAddTokenView,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Bright saturated colors for easter egg flashes (clicks 6–10)
|
||||||
|
const easterEggColors = [
|
||||||
|
"#ff0055", // hot pink
|
||||||
|
"#00cc44", // vivid green
|
||||||
|
"#3366ff", // electric blue
|
||||||
|
"#ff9900", // bright orange
|
||||||
|
"#aa00ff", // vivid purple
|
||||||
|
];
|
||||||
|
|
||||||
|
// Easter egg: click version 10 times to reveal the debug well.
|
||||||
|
// Each click does a copy-flash animation. After 5 clicks, each
|
||||||
|
// additional click flashes a different bright saturated color.
|
||||||
|
$("about-version").addEventListener("click", () => {
|
||||||
|
versionClickCount++;
|
||||||
|
clearTimeout(versionClickTimer);
|
||||||
|
// Reset counter if user stops clicking for 3 seconds
|
||||||
|
versionClickTimer = setTimeout(() => {
|
||||||
|
versionClickCount = 0;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const el = $("about-version");
|
||||||
|
|
||||||
|
if (versionClickCount > 5) {
|
||||||
|
// Colored flash for clicks 6–10
|
||||||
|
const colorIdx = versionClickCount - 6;
|
||||||
|
const color = easterEggColors[colorIdx % easterEggColors.length];
|
||||||
|
el.classList.remove("copy-flash-fade");
|
||||||
|
el.style.backgroundColor = color;
|
||||||
|
el.style.color = "#ffffff";
|
||||||
|
setTimeout(() => {
|
||||||
|
el.style.backgroundColor = "";
|
||||||
|
el.style.color = "";
|
||||||
|
el.classList.add("copy-flash-fade");
|
||||||
|
setTimeout(() => {
|
||||||
|
el.classList.remove("copy-flash-fade");
|
||||||
|
}, 275);
|
||||||
|
}, 75);
|
||||||
|
} else {
|
||||||
|
// Standard copy-flash for clicks 1–5
|
||||||
|
flashCopyFeedback(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionClickCount >= 10) {
|
||||||
|
versionClickCount = 0;
|
||||||
|
clearTimeout(versionClickTimer);
|
||||||
|
$("settings-debug-well").style.display = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debug mode toggle — update runtime flag, persist, and re-render banner
|
||||||
|
$("settings-debug-mode").addEventListener("change", async () => {
|
||||||
|
state.debugMode = $("settings-debug-mode").checked;
|
||||||
|
setRuntimeDebug(state.debugMode);
|
||||||
|
await saveState();
|
||||||
|
updateDebugBanner(state.currentView);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync runtime debug flag on init
|
||||||
|
setRuntimeDebug(state.debugMode);
|
||||||
|
|
||||||
$("btn-settings-back").addEventListener("click", () => {
|
$("btn-settings-back").addEventListener("click", () => {
|
||||||
goBack();
|
goBack();
|
||||||
});
|
});
|
||||||
|
|||||||
124
src/shared/approvalVerify.js
Normal file
124
src/shared/approvalVerify.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Verification of the signed artifacts produced by the approval popup.
|
||||||
|
//
|
||||||
|
// Signing happens in the popup, where the password is entered; the background
|
||||||
|
// only broadcasts the raw transaction and resolves the pending approval back
|
||||||
|
// to the requesting page. So that moving the signing out of the background
|
||||||
|
// does not turn the background into a blind relay, the background re-derives
|
||||||
|
// the signer from the artifact and checks it against the approval it is
|
||||||
|
// holding before acting on it. All recovery is delegated to ethers.
|
||||||
|
//
|
||||||
|
// Every failure message is a full sentence, because these strings are shown to
|
||||||
|
// the user and returned to the dApp.
|
||||||
|
|
||||||
|
const {
|
||||||
|
Transaction,
|
||||||
|
getAddress,
|
||||||
|
getBytes,
|
||||||
|
verifyMessage,
|
||||||
|
verifyTypedData,
|
||||||
|
} = require("ethers");
|
||||||
|
|
||||||
|
// Case-insensitive address comparison that tolerates absent values on either
|
||||||
|
// side. Two absent addresses compare equal (contract creation has no `to`).
|
||||||
|
function sameAddress(a, b) {
|
||||||
|
const aMissing = a === null || a === undefined || a === "";
|
||||||
|
const bMissing = b === null || b === undefined || b === "";
|
||||||
|
if (aMissing || bMissing) return aMissing && bMissing;
|
||||||
|
try {
|
||||||
|
return getAddress(a) === getAddress(b);
|
||||||
|
} catch {
|
||||||
|
return String(a).toLowerCase() === String(b).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize a transaction value (hex string, decimal string, number or
|
||||||
|
// bigint) to a bigint. An absent value is zero, matching ethers.
|
||||||
|
function normalizeValue(v) {
|
||||||
|
if (v === null || v === undefined || v === "") return 0n;
|
||||||
|
return BigInt(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize call data to a lowercase hex string. Absent data is "0x".
|
||||||
|
function normalizeData(v) {
|
||||||
|
if (v === null || v === undefined || v === "" || v === "0x") return "0x";
|
||||||
|
return String(v).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that a raw signed transaction is the transaction the user approved,
|
||||||
|
// signed by the address the approval was raised for. Returns the parsed
|
||||||
|
// ethers Transaction on success, throws otherwise.
|
||||||
|
function verifySignedTx(rawSignedTx, txParams, expectedFrom) {
|
||||||
|
if (typeof rawSignedTx !== "string" || !rawSignedTx.startsWith("0x")) {
|
||||||
|
throw new Error("The signed transaction is missing or malformed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = Transaction.from(rawSignedTx);
|
||||||
|
} catch {
|
||||||
|
throw new Error("The signed transaction could not be decoded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.from) {
|
||||||
|
throw new Error("The signed transaction carries no valid signature.");
|
||||||
|
}
|
||||||
|
if (!sameAddress(parsed.from, expectedFrom)) {
|
||||||
|
throw new Error(
|
||||||
|
"The signed transaction was signed by a different address than the one that was approved.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!sameAddress(parsed.to, txParams.to)) {
|
||||||
|
throw new Error(
|
||||||
|
"The signed transaction does not go to the approved recipient.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (normalizeValue(parsed.value) !== normalizeValue(txParams.value)) {
|
||||||
|
throw new Error(
|
||||||
|
"The signed transaction does not carry the approved value.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (normalizeData(parsed.data) !== normalizeData(txParams.data)) {
|
||||||
|
throw new Error(
|
||||||
|
"The signed transaction does not carry the approved call data.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that a signature over the approved message or typed data was
|
||||||
|
// produced by the address the approval was raised for. Returns the recovered
|
||||||
|
// address on success, throws otherwise.
|
||||||
|
function verifySignature(signParams, signature, expectedFrom) {
|
||||||
|
if (typeof signature !== "string" || !signature.startsWith("0x")) {
|
||||||
|
throw new Error("The signature is missing or malformed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let recovered;
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
signParams.method === "personal_sign" ||
|
||||||
|
signParams.method === "eth_sign"
|
||||||
|
) {
|
||||||
|
recovered = verifyMessage(getBytes(signParams.message), signature);
|
||||||
|
} else {
|
||||||
|
const typedData = JSON.parse(signParams.typedData);
|
||||||
|
const { domain, types, message } = typedData;
|
||||||
|
// ethers derives EIP712Domain itself and rejects it as an input.
|
||||||
|
delete types.EIP712Domain;
|
||||||
|
recovered = verifyTypedData(domain, types, message, signature);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
throw new Error("The signature could not be verified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sameAddress(recovered, expectedFrom)) {
|
||||||
|
throw new Error(
|
||||||
|
"The signature was produced by a different address than the one that was approved.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { verifySignedTx, verifySignature, sameAddress };
|
||||||
35
src/shared/buildInfo.js
Normal file
35
src/shared/buildInfo.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Build-time constants injected by esbuild define in build.js.
|
||||||
|
// These globals are replaced at bundle time with string literals.
|
||||||
|
|
||||||
|
/* global __BUILD_VERSION__, __BUILD_LICENSE__, __BUILD_AUTHOR__,
|
||||||
|
__BUILD_COMMIT__, __BUILD_COMMIT_FULL__, __BUILD_DATE__ */
|
||||||
|
|
||||||
|
const BUILD_VERSION =
|
||||||
|
typeof __BUILD_VERSION__ !== "undefined" ? __BUILD_VERSION__ : "dev";
|
||||||
|
const BUILD_LICENSE =
|
||||||
|
typeof __BUILD_LICENSE__ !== "undefined" ? __BUILD_LICENSE__ : "GPL-3.0";
|
||||||
|
const BUILD_AUTHOR =
|
||||||
|
typeof __BUILD_AUTHOR__ !== "undefined"
|
||||||
|
? __BUILD_AUTHOR__
|
||||||
|
: "sneak <sneak@sneak.berlin>";
|
||||||
|
const BUILD_COMMIT =
|
||||||
|
typeof __BUILD_COMMIT__ !== "undefined" ? __BUILD_COMMIT__ : "unknown";
|
||||||
|
const BUILD_COMMIT_FULL =
|
||||||
|
typeof __BUILD_COMMIT_FULL__ !== "undefined"
|
||||||
|
? __BUILD_COMMIT_FULL__
|
||||||
|
: "unknown";
|
||||||
|
const BUILD_DATE =
|
||||||
|
typeof __BUILD_DATE__ !== "undefined" ? __BUILD_DATE__ : "unknown";
|
||||||
|
|
||||||
|
const GITEA_COMMIT_URL =
|
||||||
|
"https://git.eeqj.de/sneak/AutistMask/commit/" + BUILD_COMMIT_FULL;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BUILD_VERSION,
|
||||||
|
BUILD_LICENSE,
|
||||||
|
BUILD_AUTHOR,
|
||||||
|
BUILD_COMMIT,
|
||||||
|
BUILD_COMMIT_FULL,
|
||||||
|
BUILD_DATE,
|
||||||
|
GITEA_COMMIT_URL,
|
||||||
|
};
|
||||||
@@ -1,12 +1,27 @@
|
|||||||
// Leveled logger. Outputs to console with [AutistMask] prefix.
|
// Leveled logger. Outputs to console with [AutistMask] prefix.
|
||||||
// Level is DEBUG when the DEBUG constant is true, INFO otherwise.
|
// Level is DEBUG when the compile-time DEBUG constant is true or the runtime
|
||||||
|
// debugMode state flag is enabled. The runtime flag is checked lazily so it
|
||||||
|
// responds immediately when toggled in settings.
|
||||||
|
|
||||||
const { DEBUG } = require("./constants");
|
const { DEBUG } = require("./constants");
|
||||||
|
|
||||||
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||||
const threshold = DEBUG ? LEVELS.debug : LEVELS.info;
|
|
||||||
|
// Runtime debug mode flag — set by settings.js when the user toggles debug
|
||||||
|
// mode via the easter egg. Kept here as a simple mutable reference so it can
|
||||||
|
// be updated without circular dependency issues with state.js.
|
||||||
|
let _runtimeDebug = false;
|
||||||
|
|
||||||
|
function setRuntimeDebug(enabled) {
|
||||||
|
_runtimeDebug = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDebug() {
|
||||||
|
return DEBUG || _runtimeDebug;
|
||||||
|
}
|
||||||
|
|
||||||
function emit(level, method, args) {
|
function emit(level, method, args) {
|
||||||
|
const threshold = isDebug() ? LEVELS.debug : LEVELS.info;
|
||||||
if (LEVELS[level] >= threshold) {
|
if (LEVELS[level] >= threshold) {
|
||||||
console[method]("[AutistMask]", ...args);
|
console[method]("[AutistMask]", ...args);
|
||||||
}
|
}
|
||||||
@@ -37,4 +52,4 @@ async function debugFetch(url, opts) {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { log, debugFetch };
|
module.exports = { log, debugFetch, setRuntimeDebug, isDebug };
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const DEFAULT_STATE = {
|
|||||||
fraudContracts: [],
|
fraudContracts: [],
|
||||||
tokenHolderCache: {},
|
tokenHolderCache: {},
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
debugMode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -68,6 +69,7 @@ async function saveState() {
|
|||||||
fraudContracts: state.fraudContracts,
|
fraudContracts: state.fraudContracts,
|
||||||
tokenHolderCache: state.tokenHolderCache,
|
tokenHolderCache: state.tokenHolderCache,
|
||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
|
debugMode: state.debugMode,
|
||||||
currentView: state.currentView,
|
currentView: state.currentView,
|
||||||
selectedWallet: state.selectedWallet,
|
selectedWallet: state.selectedWallet,
|
||||||
selectedAddress: state.selectedAddress,
|
selectedAddress: state.selectedAddress,
|
||||||
@@ -128,6 +130,8 @@ async function loadState() {
|
|||||||
state.fraudContracts = saved.fraudContracts || [];
|
state.fraudContracts = saved.fraudContracts || [];
|
||||||
state.tokenHolderCache = saved.tokenHolderCache || {};
|
state.tokenHolderCache = saved.tokenHolderCache || {};
|
||||||
state.theme = saved.theme || "system";
|
state.theme = saved.theme || "system";
|
||||||
|
state.debugMode =
|
||||||
|
saved.debugMode !== undefined ? saved.debugMode : false;
|
||||||
state.currentView = saved.currentView || null;
|
state.currentView = saved.currentView || null;
|
||||||
state.selectedWallet =
|
state.selectedWallet =
|
||||||
saved.selectedWallet !== undefined ? saved.selectedWallet : null;
|
saved.selectedWallet !== undefined ? saved.selectedWallet : null;
|
||||||
|
|||||||
355
tests/approvalVerify.test.js
Normal file
355
tests/approvalVerify.test.js
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
const { Network, Transaction, Wallet } = require("ethers");
|
||||||
|
const {
|
||||||
|
verifySignedTx,
|
||||||
|
verifySignature,
|
||||||
|
sameAddress,
|
||||||
|
} = require("../src/shared/approvalVerify");
|
||||||
|
const { getSignerForAddress } = require("../src/shared/wallet");
|
||||||
|
|
||||||
|
// Fixed test keys — never used for anything but these tests.
|
||||||
|
const SIGNER_KEY =
|
||||||
|
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
|
||||||
|
const OTHER_KEY =
|
||||||
|
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a";
|
||||||
|
|
||||||
|
const signer = new Wallet(SIGNER_KEY);
|
||||||
|
const other = new Wallet(OTHER_KEY);
|
||||||
|
|
||||||
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
const OTHER_RECIPIENT = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||||
|
|
||||||
|
// Approved parameters as a dApp would supply them over eth_sendTransaction.
|
||||||
|
const TX_PARAMS = {
|
||||||
|
from: signer.address,
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "0x2386f26fc10000",
|
||||||
|
data: "0xdeadbeef",
|
||||||
|
gas: "0x5208",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build a signable transaction from approved params. The popup does the same
|
||||||
|
// thing through populateTransaction(); here the fields are fixed so the test
|
||||||
|
// needs no provider.
|
||||||
|
function txFor(params) {
|
||||||
|
return {
|
||||||
|
chainId: 1,
|
||||||
|
nonce: 7,
|
||||||
|
gasLimit: 100000n,
|
||||||
|
maxFeePerGas: 2000000000n,
|
||||||
|
maxPriorityFeePerGas: 1000000000n,
|
||||||
|
type: 2,
|
||||||
|
to: params.to,
|
||||||
|
value: params.value === undefined ? 0n : BigInt(params.value),
|
||||||
|
data: params.data || "0x",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signedFor(params, withWallet) {
|
||||||
|
return (withWallet || signer).signTransaction(txFor(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sameAddress", () => {
|
||||||
|
test("compares checksummed and lowercase forms as equal", () => {
|
||||||
|
expect(sameAddress(RECIPIENT, RECIPIENT.toLowerCase())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("treats two absent addresses as equal (contract creation)", () => {
|
||||||
|
expect(sameAddress(null, undefined)).toBe(true);
|
||||||
|
expect(sameAddress("", null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("treats one absent address as unequal", () => {
|
||||||
|
expect(sameAddress(RECIPIENT, null)).toBe(false);
|
||||||
|
expect(sameAddress(null, RECIPIENT)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not throw on values that are not addresses", () => {
|
||||||
|
expect(sameAddress("not-an-address", RECIPIENT)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("verifySignedTx", () => {
|
||||||
|
test("accepts the approved transaction signed by the approved address", async () => {
|
||||||
|
const raw = await signedFor(TX_PARAMS);
|
||||||
|
const parsed = verifySignedTx(raw, TX_PARAMS, signer.address);
|
||||||
|
expect(parsed.from).toBe(signer.address);
|
||||||
|
expect(parsed.hash).toBe(Transaction.from(raw).hash);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts a contract creation with no recipient", async () => {
|
||||||
|
const params = { to: undefined, value: "0x0", data: "0x600160005500" };
|
||||||
|
const raw = await signedFor(params);
|
||||||
|
expect(() => verifySignedTx(raw, params, signer.address)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts an absent value as zero", async () => {
|
||||||
|
const approved = { to: RECIPIENT, data: "0x" };
|
||||||
|
const raw = await signedFor(approved);
|
||||||
|
expect(() =>
|
||||||
|
verifySignedTx(raw, approved, signer.address),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts call data whose case differs from the approval", async () => {
|
||||||
|
const approved = { to: RECIPIENT, value: "0x0", data: "0xDEADBEEF" };
|
||||||
|
const raw = await signedFor(approved);
|
||||||
|
expect(() =>
|
||||||
|
verifySignedTx(raw, approved, signer.address),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a swapped recipient", async () => {
|
||||||
|
const raw = await signedFor({
|
||||||
|
...TX_PARAMS,
|
||||||
|
to: OTHER_RECIPIENT,
|
||||||
|
});
|
||||||
|
expect(() => verifySignedTx(raw, TX_PARAMS, signer.address)).toThrow(
|
||||||
|
/approved recipient/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects an inflated value", async () => {
|
||||||
|
const raw = await signedFor({
|
||||||
|
...TX_PARAMS,
|
||||||
|
value: "0x4563918244f40000",
|
||||||
|
});
|
||||||
|
expect(() => verifySignedTx(raw, TX_PARAMS, signer.address)).toThrow(
|
||||||
|
/approved value/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects substituted call data", async () => {
|
||||||
|
const raw = await signedFor({ ...TX_PARAMS, data: "0xc0ffee" });
|
||||||
|
expect(() => verifySignedTx(raw, TX_PARAMS, signer.address)).toThrow(
|
||||||
|
/approved call data/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a transaction signed by a different address", async () => {
|
||||||
|
const raw = await signedFor(TX_PARAMS, other);
|
||||||
|
expect(() => verifySignedTx(raw, TX_PARAMS, signer.address)).toThrow(
|
||||||
|
/different address/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects an unsigned transaction", () => {
|
||||||
|
const unsigned = Transaction.from(txFor(TX_PARAMS)).unsignedSerialized;
|
||||||
|
expect(() =>
|
||||||
|
verifySignedTx(unsigned, TX_PARAMS, signer.address),
|
||||||
|
).toThrow(/no valid signature/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a missing or malformed payload", () => {
|
||||||
|
expect(() =>
|
||||||
|
verifySignedTx(undefined, TX_PARAMS, signer.address),
|
||||||
|
).toThrow(/missing or malformed/);
|
||||||
|
expect(() => verifySignedTx("nope", TX_PARAMS, signer.address)).toThrow(
|
||||||
|
/missing or malformed/,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
verifySignedTx("0xc0ffee", TX_PARAMS, signer.address),
|
||||||
|
).toThrow(/could not be decoded/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("every rejection message is a full sentence", async () => {
|
||||||
|
const raw = await signedFor({ ...TX_PARAMS, to: OTHER_RECIPIENT });
|
||||||
|
try {
|
||||||
|
verifySignedTx(raw, TX_PARAMS, signer.address);
|
||||||
|
throw new Error("expected a rejection");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toMatch(/^[A-Z].*\.$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const TYPED_DATA = JSON.stringify({
|
||||||
|
domain: {
|
||||||
|
name: "AutistMask Test",
|
||||||
|
version: "1",
|
||||||
|
chainId: 1,
|
||||||
|
verifyingContract: OTHER_RECIPIENT,
|
||||||
|
},
|
||||||
|
primaryType: "Mail",
|
||||||
|
types: {
|
||||||
|
EIP712Domain: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "version", type: "string" },
|
||||||
|
{ name: "chainId", type: "uint256" },
|
||||||
|
{ name: "verifyingContract", type: "address" },
|
||||||
|
],
|
||||||
|
Mail: [
|
||||||
|
{ name: "from", type: "address" },
|
||||||
|
{ name: "to", type: "address" },
|
||||||
|
{ name: "contents", type: "string" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
from: signer.address,
|
||||||
|
to: RECIPIENT,
|
||||||
|
contents: "hello",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("verifySignature", () => {
|
||||||
|
// "Hello AutistMask" as the hex string a dApp passes to personal_sign.
|
||||||
|
const MESSAGE = "0x48656c6c6f204175746973744d61736b";
|
||||||
|
const personalParams = {
|
||||||
|
method: "personal_sign",
|
||||||
|
message: MESSAGE,
|
||||||
|
from: signer.address,
|
||||||
|
};
|
||||||
|
const typedParams = {
|
||||||
|
method: "eth_signTypedData_v4",
|
||||||
|
typedData: TYPED_DATA,
|
||||||
|
from: signer.address,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function signPersonal(withWallet) {
|
||||||
|
return (withWallet || signer).signMessage(
|
||||||
|
Buffer.from(MESSAGE.slice(2), "hex"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signTyped(withWallet) {
|
||||||
|
const { domain, types, message } = JSON.parse(TYPED_DATA);
|
||||||
|
delete types.EIP712Domain;
|
||||||
|
return (withWallet || signer).signTypedData(domain, types, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("accepts a personal_sign signature from the approved address", async () => {
|
||||||
|
const signature = await signPersonal();
|
||||||
|
expect(verifySignature(personalParams, signature, signer.address)).toBe(
|
||||||
|
signer.address,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts an eth_sign signature the same way", async () => {
|
||||||
|
const signature = await signPersonal();
|
||||||
|
const params = { ...personalParams, method: "eth_sign" };
|
||||||
|
expect(() =>
|
||||||
|
verifySignature(params, signature, signer.address),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts a typed data signature from the approved address", async () => {
|
||||||
|
const signature = await signTyped();
|
||||||
|
expect(verifySignature(typedParams, signature, signer.address)).toBe(
|
||||||
|
signer.address,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not mutate the approved typed data while verifying", async () => {
|
||||||
|
const signature = await signTyped();
|
||||||
|
const before = typedParams.typedData;
|
||||||
|
verifySignature(typedParams, signature, signer.address);
|
||||||
|
expect(typedParams.typedData).toBe(before);
|
||||||
|
expect(
|
||||||
|
JSON.parse(typedParams.typedData).types.EIP712Domain,
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a personal_sign signature from a different address", async () => {
|
||||||
|
const signature = await signPersonal(other);
|
||||||
|
expect(() =>
|
||||||
|
verifySignature(personalParams, signature, signer.address),
|
||||||
|
).toThrow(/different address/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a typed data signature from a different address", async () => {
|
||||||
|
const signature = await signTyped(other);
|
||||||
|
expect(() =>
|
||||||
|
verifySignature(typedParams, signature, signer.address),
|
||||||
|
).toThrow(/different address/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a signature over a different message", async () => {
|
||||||
|
const signature = await signer.signMessage(
|
||||||
|
Buffer.from("00112233", "hex"),
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
verifySignature(personalParams, signature, signer.address),
|
||||||
|
).toThrow(/different address/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a missing or malformed signature", async () => {
|
||||||
|
expect(() =>
|
||||||
|
verifySignature(personalParams, undefined, signer.address),
|
||||||
|
).toThrow(/missing or malformed/);
|
||||||
|
expect(() =>
|
||||||
|
verifySignature(personalParams, "0x1234", signer.address),
|
||||||
|
).toThrow(/could not be verified/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// End-to-end over the messaging boundary, without a browser: run the exact
|
||||||
|
// sequence the approval popup runs, then hand the artifact to the exact check
|
||||||
|
// the background runs before it broadcasts or resolves. Only what the popup
|
||||||
|
// puts on the wire is passed along, so this also pins down that the wire
|
||||||
|
// payload is sufficient on its own.
|
||||||
|
describe("popup signing sequence to background verification", () => {
|
||||||
|
// Stand-in for the JSON-RPC provider. populateTransaction only needs the
|
||||||
|
// nonce, the gas estimate, the network and the fee data.
|
||||||
|
const fakeProvider = {
|
||||||
|
getNetwork: async () => Network.from(1),
|
||||||
|
getTransactionCount: async () => 7,
|
||||||
|
estimateGas: async () => 21000n,
|
||||||
|
getFeeData: async () => ({
|
||||||
|
gasPrice: 2000000000n,
|
||||||
|
maxFeePerGas: 2000000000n,
|
||||||
|
maxPriorityFeePerGas: 1000000000n,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// A private-key wallet as it is persisted in state, so the test goes
|
||||||
|
// through getSignerForAddress() the way the popup does.
|
||||||
|
const walletData = { type: "privkey" };
|
||||||
|
|
||||||
|
async function popupSignsTx(txParams) {
|
||||||
|
const localSigner = getSignerForAddress(walletData, 0, SIGNER_KEY);
|
||||||
|
const connected = localSigner.connect(fakeProvider);
|
||||||
|
const populated = await connected.populateTransaction(txParams);
|
||||||
|
delete populated.from;
|
||||||
|
return connected.signTransaction(populated);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a populated, signed transaction is accepted and broadcastable", async () => {
|
||||||
|
const rawSignedTx = await popupSignsTx(TX_PARAMS);
|
||||||
|
const parsed = verifySignedTx(rawSignedTx, TX_PARAMS, signer.address);
|
||||||
|
expect(parsed.nonce).toBe(7);
|
||||||
|
expect(parsed.chainId).toBe(1n);
|
||||||
|
expect(parsed.gasLimit).toBe(21000n);
|
||||||
|
expect(parsed.to).toBe(RECIPIENT);
|
||||||
|
expect(parsed.value).toBe(BigInt(TX_PARAMS.value));
|
||||||
|
expect(parsed.data).toBe(TX_PARAMS.data);
|
||||||
|
expect(parsed.signature).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the wire payload carries no password and no secret", async () => {
|
||||||
|
const rawSignedTx = await popupSignsTx(TX_PARAMS);
|
||||||
|
const payload = {
|
||||||
|
type: "AUTISTMASK_TX_RESPONSE",
|
||||||
|
id: "test-approval-id",
|
||||||
|
approved: true,
|
||||||
|
rawSignedTx,
|
||||||
|
};
|
||||||
|
expect(Object.keys(payload).sort()).toEqual([
|
||||||
|
"approved",
|
||||||
|
"id",
|
||||||
|
"rawSignedTx",
|
||||||
|
"type",
|
||||||
|
]);
|
||||||
|
const wire = JSON.stringify(payload).toLowerCase();
|
||||||
|
expect(wire).not.toContain("password");
|
||||||
|
expect(wire).not.toContain(SIGNER_KEY.slice(2).toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the background rejects a transaction the popup did not approve", async () => {
|
||||||
|
const rawSignedTx = await popupSignsTx({
|
||||||
|
...TX_PARAMS,
|
||||||
|
to: OTHER_RECIPIENT,
|
||||||
|
});
|
||||||
|
expect(() =>
|
||||||
|
verifySignedTx(rawSignedTx, TX_PARAMS, signer.address),
|
||||||
|
).toThrow(/approved recipient/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user