Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248057a25d |
@@ -1844,11 +1844,7 @@ view would leave a wallet one click from deletion.
|
|||||||
says nothing about `http://dapp.example` or another port of that host. The
|
says nothing about `http://dapp.example` or another port of that host. The
|
||||||
background script prefers the toolbar popup (`action.openPopup()`) and falls
|
background script prefers the toolbar popup (`action.openPopup()`) and falls
|
||||||
back to a separate popup window (`src/background/index.js`,
|
back to a separate popup window (`src/background/index.js`,
|
||||||
`requestApproval()`). Only one exists per site at a time: a further connection
|
`requestApproval()`).
|
||||||
request from a site whose prompt is still unanswered is refused with EIP-1193
|
|
||||||
code `-32002` and opens no new prompt. If that prompt was in a toolbar popup
|
|
||||||
that closed before it connected, and the toolbar popup has since been set to
|
|
||||||
open something else, the refused request shows that prompt again.
|
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Connection Request" heading
|
- "Connection Request" heading
|
||||||
- Phishing warning banner (shown when the hostname is on the phishing
|
- Phishing warning banner (shown when the hostname is on the phishing
|
||||||
@@ -1914,10 +1910,7 @@ view would leave a wallet one click from deletion.
|
|||||||
|
|
||||||
- **When**: A connected website requests a message signature via
|
- **When**: A connected website requests a message signature via
|
||||||
`personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. Opened the same way as
|
`personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. Opened the same way as
|
||||||
TxApproval, in a separate popup window. Only one exists per site at a time: a
|
TxApproval, in a separate popup window.
|
||||||
further signature request, by any of these methods, from a site whose
|
|
||||||
signature request is still unanswered is refused with EIP-1193 code `-32002`
|
|
||||||
and opens no window.
|
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Signature Request" heading
|
- "Signature Request" heading
|
||||||
- Phishing warning banner (shown when the hostname is on the phishing
|
- Phishing warning banner (shown when the hostname is on the phishing
|
||||||
@@ -1928,16 +1921,14 @@ view would leave a wallet one click from deletion.
|
|||||||
- Type: "Personal message" or "Typed data (EIP-712)"
|
- Type: "Personal message" or "Typed data (EIP-712)"
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
- Message: for `personal_sign` and `eth_sign`, the text the message's bytes
|
- Message: for `personal_sign` and `eth_sign`, the text the message's bytes
|
||||||
decode to as UTF-8, laid out left to right in the order of the bytes that
|
decode to as UTF-8, with each control or format character (zero-width and
|
||||||
are signed, right-to-left characters included. Each control character, and
|
bidirectional characters among them) shown as a bordered `U+XXXX` mark
|
||||||
each character that paints nothing (format characters such as zero-width
|
instead of acting on the text, so it reads in the order of the bytes that
|
||||||
and bidirectional ones, default-ignorable characters such as variation
|
are signed; a line feed is shown as a line break. Bytes that are not UTF-8
|
||||||
selectors and Hangul fillers, and DELETE), is shown as a bordered `U+XXXX`
|
are shown as "This message is not text." For typed data, formatted
|
||||||
mark instead of acting on the text; a line feed is shown as a line break.
|
domain/type/message fields (EIP-712). The primary type shown is the one
|
||||||
Bytes that are not UTF-8 are shown as "This message is not text." For
|
ethers signs, derived from the typed data's `types`, not the type the site
|
||||||
typed data, formatted domain/type/message fields (EIP-712). The primary
|
states.
|
||||||
type shown is the one ethers signs, derived from the typed data's `types`,
|
|
||||||
not the type the site states.
|
|
||||||
- Token permission warning, at the top of the message (typed data whose
|
- Token permission warning, at the top of the message (typed data whose
|
||||||
primary type is `Permit`, as in EIP-2612, or one of Permit2's signature
|
primary type is `Permit`, as in EIP-2612, or one of Permit2's signature
|
||||||
types): "⚠️ TOKEN PERMISSION: Signing this lets the spender below take the
|
types): "⚠️ TOKEN PERMISSION: Signing this lets the spender below take the
|
||||||
@@ -1958,11 +1949,11 @@ view would leave a wallet one click from deletion.
|
|||||||
- Typed data that states no primary type, or one other than the type it
|
- Typed data that states no primary type, or one other than the type it
|
||||||
would be signed as, or that cannot be read → shown with the error line
|
would be signed as, or that cannot be read → shown with the error line
|
||||||
saying so and "Sign" disabled; only "Reject" remains
|
saying so and "Sign" disabled; only "Reject" remains
|
||||||
- A `personal_sign` or `eth_sign` message that is not hex (`0x` or `0X` and
|
- A `personal_sign` or `eth_sign` message that is not hex (`0x` and an even
|
||||||
an even number of hex digits, the form ethers' `getBytes` reads when
|
number of hex digits) → shown as plain text, with the error line "This
|
||||||
signing) → shown as plain text, with the error line "This message is plain
|
message is plain text, not hex, so it cannot be signed." and "Sign"
|
||||||
text, not hex, so it cannot be signed." and "Sign" disabled; signing takes
|
disabled; signing takes the bytes from the hex, so such a message has none
|
||||||
the bytes from the hex, so such a message has none to sign
|
to sign
|
||||||
- "Sign" (correct password) → signs locally → closes popup (returns
|
- "Sign" (correct password) → signs locally → closes popup (returns
|
||||||
signature)
|
signature)
|
||||||
- "Sign" (wrong password, or a signing failure) → error line, no screen
|
- "Sign" (wrong password, or a signing failure) → error line, no screen
|
||||||
|
|||||||
@@ -50,23 +50,10 @@ but the review is broader than any of them.
|
|||||||
showed only the decoded text, with bidirectional and zero-width characters
|
showed only the decoded text, with bidirectional and zero-width characters
|
||||||
acting on it, so a site could make the message read differently from what is
|
acting on it, so a site could make the message read differently from what is
|
||||||
signed, and a message that was not hex was shown as NUL characters. The hex is
|
signed, and a message that was not hex was shown as NUL characters. The hex is
|
||||||
now shown as "Raw data" alongside the decoded text, the text is laid out left
|
now shown as "Raw data" alongside the decoded text, control and format
|
||||||
to right in byte order, control characters and characters that paint nothing
|
characters in the text are shown as `U+XXXX` marks, and a message that is not
|
||||||
are shown as `U+XXXX` marks, and a message that is not hex by the rule signing
|
hex is shown as plain text with "Sign" disabled, since signing takes the bytes
|
||||||
reads it with is shown as plain text with "Sign" disabled, since such a
|
from the hex and such a message has none to sign.
|
||||||
message has no bytes to sign.
|
|
||||||
|
|
||||||
- 2026-10-04: A site has at most one connection prompt and one signature prompt
|
|
||||||
open at a time ([#405](https://git.eeqj.de/sneak/AutistMask/issues/405)). Each
|
|
||||||
`eth_requestAccounts` or `personal_sign` call opened another approval window,
|
|
||||||
so a page calling in a loop could cover the screen with identical prompts. A
|
|
||||||
further request of the same kind from a site whose prompt is still unanswered
|
|
||||||
is now refused with EIP-1193 `-32002`, the code a second transaction already
|
|
||||||
gets, and opens no window. Signing by `personal_sign`, `eth_sign` and
|
|
||||||
`eth_signTypedData_v4` counts as one kind. Other sites are not affected, and
|
|
||||||
the site may ask again once the user has answered. A connection prompt whose
|
|
||||||
toolbar popup closed before it connected, and which nothing shows any more, is
|
|
||||||
shown again when the site asks again.
|
|
||||||
|
|
||||||
- 2026-10-04: A nonce the site supplies with `eth_sendTransaction` is ignored
|
- 2026-10-04: A nonce the site supplies with `eth_sendTransaction` is ignored
|
||||||
([#404](https://git.eeqj.de/sneak/AutistMask/issues/404)). It was passed on to
|
([#404](https://git.eeqj.de/sneak/AutistMask/issues/404)). It was passed on to
|
||||||
|
|||||||
+15
-85
@@ -87,8 +87,7 @@ const pendingApprovals = {};
|
|||||||
// authority on a nonce the network has not accepted, which an abandoned
|
// authority on a nonce the network has not accepted, which an abandoned
|
||||||
// approval then leaves a hole in.
|
// approval then leaves a hole in.
|
||||||
//
|
//
|
||||||
// Sign approvals do not take this slot: a signature consumes no nonce. They are
|
// Sign approvals are not gated: a signature consumes no nonce.
|
||||||
// limited per site instead; see findPendingApproval().
|
|
||||||
//
|
//
|
||||||
// The slot is null when free, and otherwise the handle of the request holding
|
// The slot is null when free, and otherwise the handle of the request holding
|
||||||
// it. Once that request has raised its approval the handle carries the
|
// it. Once that request has raised its approval the handle carries the
|
||||||
@@ -100,7 +99,7 @@ let txApprovalSlot = null;
|
|||||||
|
|
||||||
// EIP-1474 "resource unavailable": the standard code for a request that is
|
// EIP-1474 "resource unavailable": the standard code for a request that is
|
||||||
// refused because another one is already pending.
|
// refused because another one is already pending.
|
||||||
const APPROVAL_PENDING_CODE = -32002;
|
const TX_APPROVAL_PENDING_CODE = -32002;
|
||||||
|
|
||||||
// True at every moment this can be sent: the slot is taken immediately before
|
// True at every moment this can be sent: the slot is taken immediately before
|
||||||
// the transaction is populated, so the other request is either being prepared
|
// the transaction is populated, so the other request is either being prepared
|
||||||
@@ -137,22 +136,6 @@ function releaseTxApprovalSlotFor(approvalId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One site-connection approval and one sign approval per site at a time: a
|
|
||||||
// page that asks again before the user has answered is refused with the code
|
|
||||||
// above instead of opening another window, so it cannot bury the user in
|
|
||||||
// prompts. The pending approval itself holds the place, so a caller must test
|
|
||||||
// this and raise its approval with nothing awaited in between.
|
|
||||||
function findPendingApproval(origin, type) {
|
|
||||||
return Object.values(pendingApprovals).find(
|
|
||||||
(approval) => approval.origin === origin && approval.type === type,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const APPROVAL_PENDING_MESSAGE =
|
|
||||||
"AutistMask is already waiting for your answer to a request of this kind" +
|
|
||||||
" from this site, so this one was not shown. Please answer that one," +
|
|
||||||
" then send this one again.";
|
|
||||||
|
|
||||||
// Nonces this worker has already handed to the node, per chain and address.
|
// Nonces this worker has already handed to the node, per chain and address.
|
||||||
// This is the wallet's own knowledge that a nonce is spent, and it is checked
|
// This is the wallet's own knowledge that a nonce is spent, and it is checked
|
||||||
// before a broadcast rather than after: a node's pending count can lag a
|
// before a broadcast rather than after: a node's pending count can lag a
|
||||||
@@ -305,12 +288,7 @@ async function proxyRpc(method, params) {
|
|||||||
return json.result;
|
return json.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The site-connection approval the toolbar popup is set to open, or null while
|
|
||||||
// it opens the wallet. Set only by resetPopupUrl() and showInToolbarPopup().
|
|
||||||
let toolbarPopupApprovalId = null;
|
|
||||||
|
|
||||||
function resetPopupUrl() {
|
function resetPopupUrl() {
|
||||||
toolbarPopupApprovalId = null;
|
|
||||||
if (actionNs && typeof actionNs.setPopup === "function") {
|
if (actionNs && typeof actionNs.setPopup === "function") {
|
||||||
actionNs.setPopup({ popup: "src/popup/index.html" });
|
actionNs.setPopup({ popup: "src/popup/index.html" });
|
||||||
}
|
}
|
||||||
@@ -488,33 +466,26 @@ async function openApprovalWindow(id) {
|
|||||||
function requestApproval(origin) {
|
function requestApproval(origin) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
pendingApprovals[id] = { id, origin, resolve, type: "site" };
|
pendingApprovals[id] = { id, origin, resolve };
|
||||||
|
|
||||||
if (actionNs && typeof actionNs.openPopup === "function") {
|
if (actionNs && typeof actionNs.openPopup === "function") {
|
||||||
showInToolbarPopup(id);
|
actionNs.setPopup({
|
||||||
|
popup: "src/popup/index.html?approval=" + id,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = actionNs.openPopup();
|
||||||
|
if (result && typeof result.catch === "function") {
|
||||||
|
result.catch(() => openApprovalWindow(id));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
openApprovalWindow(id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
openApprovalWindow(id);
|
openApprovalWindow(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a site-connection approval in the toolbar popup, or in a separate popup
|
|
||||||
// window when the browser will not open the toolbar popup.
|
|
||||||
function showInToolbarPopup(id) {
|
|
||||||
toolbarPopupApprovalId = id;
|
|
||||||
actionNs.setPopup({
|
|
||||||
popup: "src/popup/index.html?approval=" + id,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const result = actionNs.openPopup();
|
|
||||||
if (result && typeof result.catch === "function") {
|
|
||||||
result.catch(() => openApprovalWindow(id));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
openApprovalWindow(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open a tx-approval popup and return a promise that resolves with txHash or error.
|
// Open a tx-approval popup and return a promise that resolves with txHash or error.
|
||||||
// Uses windows.create() directly because tx approvals are triggered programmatically
|
// Uses windows.create() directly because tx approvals are triggered programmatically
|
||||||
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
|
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
|
||||||
@@ -670,31 +641,6 @@ async function handleConnectionRequest(origin) {
|
|||||||
return { result: [activeAddress] };
|
return { result: [activeAddress] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = findPendingApproval(origin, "site");
|
|
||||||
if (pending) {
|
|
||||||
// A toolbar popup that closed before it connected leaves its prompt
|
|
||||||
// pending, and once the toolbar popup is set to open something else
|
|
||||||
// nothing shows that prompt: the site would be refused until the
|
|
||||||
// address changed. Show it again. A prompt in a window or in a
|
|
||||||
// connected popup is settled when that closes, and one the toolbar
|
|
||||||
// popup is still set to open is a click away, so those are left alone.
|
|
||||||
if (
|
|
||||||
actionNs &&
|
|
||||||
typeof actionNs.openPopup === "function" &&
|
|
||||||
!pending.windowId &&
|
|
||||||
!pending.portConnected &&
|
|
||||||
toolbarPopupApprovalId !== pending.id
|
|
||||||
) {
|
|
||||||
showInToolbarPopup(pending.id);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: APPROVAL_PENDING_CODE,
|
|
||||||
message: APPROVAL_PENDING_MESSAGE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open approval popup
|
// Open approval popup
|
||||||
const decision = await requestApproval(origin);
|
const decision = await requestApproval(origin);
|
||||||
|
|
||||||
@@ -919,14 +865,6 @@ async function handleRpc(method, params, origin) {
|
|||||||
"Only proceed if you fully understand what you are signing.";
|
"Only proceed if you fully understand what you are signing.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (findPendingApproval(origin, "sign")) {
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: APPROVAL_PENDING_CODE,
|
|
||||||
message: APPROVAL_PENDING_MESSAGE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const decision = await requestSignApproval(
|
const decision = await requestSignApproval(
|
||||||
origin,
|
origin,
|
||||||
signParams,
|
signParams,
|
||||||
@@ -960,14 +898,6 @@ async function handleRpc(method, params, origin) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (findPendingApproval(origin, "sign")) {
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: APPROVAL_PENDING_CODE,
|
|
||||||
message: APPROVAL_PENDING_MESSAGE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const decision = await requestSignApproval(
|
const decision = await requestSignApproval(
|
||||||
origin,
|
origin,
|
||||||
signParams,
|
signParams,
|
||||||
@@ -1039,7 +969,7 @@ async function handleSendTransaction(params, origin) {
|
|||||||
if (!slot) {
|
if (!slot) {
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
code: APPROVAL_PENDING_CODE,
|
code: TX_APPROVAL_PENDING_CODE,
|
||||||
message: TX_APPROVAL_PENDING_MESSAGE,
|
message: TX_APPROVAL_PENDING_MESSAGE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,12 +64,3 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A personal message on the signature screen is laid out left to right in
|
|
||||||
* the order of its bytes. Without this, right-to-left characters in it move
|
|
||||||
* the characters around them: `5`, U+05C3, `00` would read as `500`
|
|
||||||
* followed by U+05C3. */
|
|
||||||
.am-byte-order {
|
|
||||||
direction: ltr;
|
|
||||||
unicode-bidi: bidi-override;
|
|
||||||
}
|
|
||||||
|
|||||||
+17
-36
@@ -20,13 +20,13 @@ const {
|
|||||||
getBigInt,
|
getBigInt,
|
||||||
getBytes,
|
getBytes,
|
||||||
Interface,
|
Interface,
|
||||||
|
isHexString,
|
||||||
MaxUint256,
|
MaxUint256,
|
||||||
toUtf8String,
|
toUtf8String,
|
||||||
TypedDataEncoder,
|
TypedDataEncoder,
|
||||||
} = require("ethers");
|
} = 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 { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
|
||||||
const {
|
const {
|
||||||
resolveTokenDecimals,
|
resolveTokenDecimals,
|
||||||
resolveTokenSymbol,
|
resolveTokenSymbol,
|
||||||
@@ -381,17 +381,6 @@ function showTxApproval(details) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether a personal message is hex by the rule signing reads it with:
|
|
||||||
// signing takes getBytes(message), which throws on anything else.
|
|
||||||
function isHexMessage(message) {
|
|
||||||
try {
|
|
||||||
getBytes(message);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The text the hex message's bytes decode to as UTF-8, or null when they are
|
// The text the hex message's bytes decode to as UTF-8, or null when they are
|
||||||
// not UTF-8. The caller has checked that the message is hex.
|
// not UTF-8. The caller has checked that the message is hex.
|
||||||
function decodeHexMessage(hex) {
|
function decodeHexMessage(hex) {
|
||||||
@@ -402,20 +391,16 @@ function decodeHexMessage(hex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A character shown as a bordered U+XXXX mark.
|
// The text as HTML, with each control or format character (zero-width and
|
||||||
function codePointMark(c) {
|
// bidirectional characters among them) shown as a bordered U+XXXX mark
|
||||||
const code = c.codePointAt(0).toString(16).toUpperCase();
|
// instead of acting on the text, so it reads in the order of its bytes. Line
|
||||||
return `<span class="border border-border">U+${code.padStart(4, "0")}</span>`;
|
// feeds are shown as line breaks.
|
||||||
}
|
function markControlCharacters(text) {
|
||||||
|
return escapeHtml(text).replace(/[\p{Cc}\p{Cf}]/gu, (c) => {
|
||||||
// The text as HTML, with each character that paints nothing (zero-width and
|
if (c === "\n") return "<br>";
|
||||||
// bidirectional characters, variation selectors and Hangul fillers among
|
const code = c.codePointAt(0).toString(16).toUpperCase();
|
||||||
// them) and each control character shown as a mark. A line feed is shown as
|
return `<span class="border border-border">U+${code.padStart(4, "0")}</span>`;
|
||||||
// a line break. The marks are plain ASCII, so the second pass leaves them be.
|
});
|
||||||
function markInvisibleCharacters(text) {
|
|
||||||
return escapeHtml(text)
|
|
||||||
.replace(INVISIBLE_CHARACTERS, codePointMark)
|
|
||||||
.replace(/\p{Cc}/gu, (c) => (c === "\n" ? "<br>" : codePointMark(c)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The type ethers will sign typed data as. ethers does not read the page's
|
// The type ethers will sign typed data as. ethers does not read the page's
|
||||||
@@ -682,30 +667,26 @@ function showSignApproval(details) {
|
|||||||
: "Personal message";
|
: "Personal message";
|
||||||
|
|
||||||
// A personal message is signed as the bytes its hex encodes, so the hex
|
// A personal message is signed as the bytes its hex encodes, so the hex
|
||||||
// is shown as well as any text it decodes to, and that text is laid out
|
// is shown as well as any text it decodes to. Signing reads the bytes
|
||||||
// left to right in the order of its bytes. Signing reads the bytes from
|
// from the hex, so a message that is not hex cannot be signed: it is
|
||||||
// the hex, so a message that is not hex cannot be signed: it is shown as
|
// shown as the text it is, and refused.
|
||||||
// the text it is, and refused.
|
|
||||||
let refusal = null;
|
let refusal = null;
|
||||||
$("approve-sign-hex-section").classList.add("hidden");
|
$("approve-sign-hex-section").classList.add("hidden");
|
||||||
$("approve-sign-message").classList.toggle("am-byte-order", !isTyped);
|
|
||||||
if (isTyped) {
|
if (isTyped) {
|
||||||
$("approve-sign-message").innerHTML = formatTypedDataHtml(sp.typedData);
|
$("approve-sign-message").innerHTML = formatTypedDataHtml(sp.typedData);
|
||||||
refusal = typedDataRefusal(sp);
|
refusal = typedDataRefusal(sp);
|
||||||
} else if (isHexMessage(sp.message)) {
|
} else if (isHexString(sp.message, true)) {
|
||||||
const decoded = decodeHexMessage(sp.message);
|
const decoded = decodeHexMessage(sp.message);
|
||||||
if (decoded !== null) {
|
if (decoded !== null) {
|
||||||
$("approve-sign-message").innerHTML =
|
$("approve-sign-message").innerHTML =
|
||||||
markInvisibleCharacters(decoded);
|
markControlCharacters(decoded);
|
||||||
} else {
|
} else {
|
||||||
$("approve-sign-message").textContent = "This message is not text.";
|
$("approve-sign-message").textContent = "This message is not text.";
|
||||||
}
|
}
|
||||||
$("approve-sign-hex").textContent = sp.message;
|
$("approve-sign-hex").textContent = sp.message;
|
||||||
$("approve-sign-hex-section").classList.remove("hidden");
|
$("approve-sign-hex-section").classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
$("approve-sign-message").innerHTML = markInvisibleCharacters(
|
$("approve-sign-message").innerHTML = markControlCharacters(sp.message);
|
||||||
sp.message,
|
|
||||||
);
|
|
||||||
refusal =
|
refusal =
|
||||||
"This message is plain text, not hex, so it cannot be signed.";
|
"This message is plain text, not hex, so it cannot be signed.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ function normalizeAddress(addr) {
|
|||||||
return (addr || "").toLowerCase();
|
return (addr || "").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The characters that paint nothing; normalizeSymbol below says which they
|
|
||||||
// are. The signature screen marks them in a personal message
|
|
||||||
// (src/popup/views/approval.js).
|
|
||||||
const INVISIBLE_CHARACTERS = /[\p{Cf}\p{Default_Ignorable_Code_Point}\x7F]/gu;
|
|
||||||
|
|
||||||
// Fold a symbol onto what a user actually sees, and no further:
|
// Fold a symbol onto what a user actually sees, and no further:
|
||||||
//
|
//
|
||||||
// NFKC collapses compatibility variants that render as the ASCII
|
// NFKC collapses compatibility variants that render as the ASCII
|
||||||
@@ -87,7 +82,7 @@ const INVISIBLE_CHARACTERS = /[\p{Cf}\p{Default_Ignorable_Code_Point}\x7F]/gu;
|
|||||||
function normalizeSymbol(symbol) {
|
function normalizeSymbol(symbol) {
|
||||||
return String(symbol || "")
|
return String(symbol || "")
|
||||||
.normalize("NFKC")
|
.normalize("NFKC")
|
||||||
.replace(INVISIBLE_CHARACTERS, "")
|
.replace(/[\p{Cf}\p{Default_Ignorable_Code_Point}\x7F]/gu, "")
|
||||||
.trim()
|
.trim()
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
@@ -109,6 +104,5 @@ function isSpoofedSymbol(symbol, contractAddress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
INVISIBLE_CHARACTERS,
|
|
||||||
isSpoofedSymbol,
|
isSpoofedSymbol,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,22 +74,6 @@ const NONCE = 7;
|
|||||||
// "Hello AutistMask" as the hex string a dApp passes to personal_sign.
|
// "Hello AutistMask" as the hex string a dApp passes to personal_sign.
|
||||||
const MESSAGE = "0x48656c6c6f204175746973744d61736b";
|
const MESSAGE = "0x48656c6c6f204175746973744d61736b";
|
||||||
|
|
||||||
// An EIP-712 document as a dApp passes it to eth_signTypedData_v4. The
|
|
||||||
// background only carries it to the approval screen, so a small one does.
|
|
||||||
const TYPED_DATA = JSON.stringify({
|
|
||||||
domain: { name: "AutistMask Test", version: "1", chainId: 1 },
|
|
||||||
primaryType: "Note",
|
|
||||||
types: {
|
|
||||||
EIP712Domain: [
|
|
||||||
{ name: "name", type: "string" },
|
|
||||||
{ name: "version", type: "string" },
|
|
||||||
{ name: "chainId", type: "uint256" },
|
|
||||||
],
|
|
||||||
Note: [{ name: "contents", type: "string" }],
|
|
||||||
},
|
|
||||||
message: { contents: "Hello AutistMask" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// The transaction the background populates and the approval screen displays.
|
// The transaction the background populates and the approval screen displays.
|
||||||
// The nonce is a parameter because the duplicate case turns on two artifacts
|
// The nonce is a parameter because the duplicate case turns on two artifacts
|
||||||
// differing in a field the dApp fixed nothing for.
|
// differing in a field the dApp fixed nothing for.
|
||||||
@@ -245,7 +229,6 @@ function loadBackground(options) {
|
|||||||
// raised through action.openPopup() opens no window at all, so this is
|
// raised through action.openPopup() opens no window at all, so this is
|
||||||
// the only place its id appears.
|
// the only place its id appears.
|
||||||
const actionPopups = [];
|
const actionPopups = [];
|
||||||
const openPopup = jest.fn(() => Promise.resolve());
|
|
||||||
|
|
||||||
global.chrome = {
|
global.chrome = {
|
||||||
storage,
|
storage,
|
||||||
@@ -300,7 +283,7 @@ function loadBackground(options) {
|
|||||||
// popup: no window is created, so windows.onRemoved can never
|
// popup: no window is created, so windows.onRemoved can never
|
||||||
// fire for it and the port disconnect is the only close signal
|
// fire for it and the port disconnect is the only close signal
|
||||||
// that exists.
|
// that exists.
|
||||||
...(opts.actionPopup ? { openPopup } : {}),
|
...(opts.actionPopup ? { openPopup: () => Promise.resolve() } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -347,7 +330,7 @@ function loadBackground(options) {
|
|||||||
|
|
||||||
// The same for a message-signing approval, which pins the signing address
|
// The same for a message-signing approval, which pins the signing address
|
||||||
// at approval time in exactly the same way.
|
// at approval time in exactly the same way.
|
||||||
function requestSign(from, origin) {
|
function requestSign(from) {
|
||||||
let rpcResult = null;
|
let rpcResult = null;
|
||||||
messageListener(
|
messageListener(
|
||||||
{
|
{
|
||||||
@@ -355,7 +338,7 @@ function loadBackground(options) {
|
|||||||
method: "personal_sign",
|
method: "personal_sign",
|
||||||
params: [MESSAGE, from || signer.address],
|
params: [MESSAGE, from || signer.address],
|
||||||
},
|
},
|
||||||
{ origin: origin || ORIGIN },
|
{ origin: ORIGIN },
|
||||||
(r) => {
|
(r) => {
|
||||||
rpcResult = r;
|
rpcResult = r;
|
||||||
},
|
},
|
||||||
@@ -369,24 +352,6 @@ function loadBackground(options) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same through eth_signTypedData_v4, whose params name the address
|
|
||||||
// first and the typed data second.
|
|
||||||
function requestTypedData() {
|
|
||||||
let rpcResult = null;
|
|
||||||
messageListener(
|
|
||||||
{
|
|
||||||
type: "AUTISTMASK_RPC",
|
|
||||||
method: "eth_signTypedData_v4",
|
|
||||||
params: [signer.address, TYPED_DATA],
|
|
||||||
},
|
|
||||||
{ origin: ORIGIN },
|
|
||||||
(r) => {
|
|
||||||
rpcResult = r;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return { result: () => rpcResult };
|
|
||||||
}
|
|
||||||
|
|
||||||
// A dApp asking to connect. The origin defaults to one the persisted
|
// A dApp asking to connect. The origin defaults to one the persisted
|
||||||
// state has never allowed, so the request really does raise a prompt
|
// state has never allowed, so the request really does raise a prompt
|
||||||
// instead of being answered from allowedSites.
|
// instead of being answered from allowedSites.
|
||||||
@@ -461,15 +426,12 @@ function loadBackground(options) {
|
|||||||
send,
|
send,
|
||||||
requestTx,
|
requestTx,
|
||||||
requestSign,
|
requestSign,
|
||||||
requestTypedData,
|
|
||||||
requestSite,
|
requestSite,
|
||||||
connectApproval,
|
connectApproval,
|
||||||
closeWindow,
|
closeWindow,
|
||||||
broadcastTransaction,
|
broadcastTransaction,
|
||||||
created,
|
created,
|
||||||
removed,
|
removed,
|
||||||
actionPopups,
|
|
||||||
openPopup,
|
|
||||||
storage,
|
storage,
|
||||||
// The user switching account in the toolbar popup, as the background
|
// The user switching account in the toolbar popup, as the background
|
||||||
// sees it: the persisted active address changes underneath a pending
|
// sees it: the persisted active address changes underneath a pending
|
||||||
@@ -859,238 +821,6 @@ describe("one transaction approval at a time", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// A page that asks again before the user has answered its last connection or
|
|
||||||
// signature request is refused, instead of opening one more window per call.
|
|
||||||
describe("one connection and one signature approval per site at a time", () => {
|
|
||||||
const PENDING_REFUSAL = {
|
|
||||||
error: {
|
|
||||||
code: -32002,
|
|
||||||
message: expect.stringMatching(/already waiting for your answer/),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
test("a loop of eth_requestAccounts opens one approval and refuses the rest", async () => {
|
|
||||||
const bg = loadBackground();
|
|
||||||
const requests = [];
|
|
||||||
for (let i = 0; i < 5; i++) requests.push(bg.requestSite());
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(bg.created).toHaveLength(1);
|
|
||||||
expect(requests[0].result()).toBeNull();
|
|
||||||
for (const extra of requests.slice(1)) {
|
|
||||||
expect(extra.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the user has answered, the site may ask again.
|
|
||||||
bg.closeWindow(1);
|
|
||||||
await settle();
|
|
||||||
expect(requests[0].result()).toEqual({
|
|
||||||
error: { code: 4001, message: "User rejected the request." },
|
|
||||||
});
|
|
||||||
const again = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
expect(again.result()).toBeNull();
|
|
||||||
expect(bg.created).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a loop of personal_sign opens one approval and refuses the rest", async () => {
|
|
||||||
const bg = loadBackground();
|
|
||||||
const requests = [];
|
|
||||||
for (let i = 0; i < 5; i++) requests.push(bg.requestSign());
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(bg.created).toHaveLength(1);
|
|
||||||
expect(requests[0].result()).toBeNull();
|
|
||||||
for (const extra of requests.slice(1)) {
|
|
||||||
expect(extra.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a loop of eth_signTypedData_v4 opens one approval and refuses the rest", async () => {
|
|
||||||
const bg = loadBackground();
|
|
||||||
const requests = [];
|
|
||||||
for (let i = 0; i < 5; i++) requests.push(bg.requestTypedData());
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(bg.created).toHaveLength(1);
|
|
||||||
expect(requests[0].result()).toBeNull();
|
|
||||||
for (const extra of requests.slice(1)) {
|
|
||||||
expect(extra.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a pending personal_sign also refuses eth_signTypedData_v4", async () => {
|
|
||||||
const bg = loadBackground();
|
|
||||||
bg.requestSign();
|
|
||||||
const typed = bg.requestTypedData();
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(typed.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(bg.created).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("in the toolbar popup, a loop of eth_requestAccounts opens it once", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
const requests = [];
|
|
||||||
for (let i = 0; i < 5; i++) requests.push(bg.requestSite());
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(bg.openPopup).toHaveBeenCalledTimes(1);
|
|
||||||
for (const extra of requests.slice(1)) {
|
|
||||||
expect(extra.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// A toolbar popup that closes before it connects tells the background
|
|
||||||
// nothing, so its prompt stays pending. Once the toolbar popup has been set
|
|
||||||
// to open something else, nothing shows that prompt, and the site asking
|
|
||||||
// again must show it again rather than be refused for good.
|
|
||||||
test("a toolbar prompt nothing shows any more is shown again when the site asks again", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
const first = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
const id = first.id();
|
|
||||||
|
|
||||||
// Its popup closed without connecting. Another site's prompt takes the
|
|
||||||
// toolbar popup and is answered, which sets it back to the wallet.
|
|
||||||
const other = bg.requestSite(UNCONNECTED_ORIGIN);
|
|
||||||
await settle();
|
|
||||||
const otherPort = bg.connectApproval(other.id());
|
|
||||||
otherPort.decide(false, false);
|
|
||||||
otherPort.disconnect();
|
|
||||||
await settle();
|
|
||||||
expect(bg.actionPopups[bg.actionPopups.length - 1]).toBe(
|
|
||||||
"src/popup/index.html",
|
|
||||||
);
|
|
||||||
|
|
||||||
const repeat = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
expect(repeat.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(bg.openPopup).toHaveBeenCalledTimes(3);
|
|
||||||
expect(bg.actionPopups[bg.actionPopups.length - 1]).toBe(
|
|
||||||
"src/popup/index.html?approval=" + id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// The user answers it, and the first request gets that answer.
|
|
||||||
bg.connectApproval(id).decide(true, false);
|
|
||||||
await settle();
|
|
||||||
expect(first.result()).toEqual({ result: [signer.address] });
|
|
||||||
});
|
|
||||||
|
|
||||||
// The user rejects a signature request from another site, which is
|
|
||||||
// connected already. Answering any approval sets the toolbar popup back to
|
|
||||||
// the wallet.
|
|
||||||
async function rejectSignatureFromAnotherSite(bg) {
|
|
||||||
const sign = bg.requestSign(undefined, ORIGIN);
|
|
||||||
await settle();
|
|
||||||
bg.send(
|
|
||||||
{
|
|
||||||
type: "AUTISTMASK_SIGN_RESPONSE",
|
|
||||||
id: sign.id(),
|
|
||||||
approved: false,
|
|
||||||
},
|
|
||||||
{ url: bg.fromPopup.url },
|
|
||||||
);
|
|
||||||
await settle();
|
|
||||||
expect(sign.result()).toEqual({
|
|
||||||
error: { code: 4001, message: "User rejected the request." },
|
|
||||||
});
|
|
||||||
expect(bg.actionPopups[bg.actionPopups.length - 1]).toBe(
|
|
||||||
"src/popup/index.html",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("a toolbar prompt is shown again after another site's signature request is answered", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
const first = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
const id = first.id();
|
|
||||||
|
|
||||||
// Its popup closed without connecting.
|
|
||||||
await rejectSignatureFromAnotherSite(bg);
|
|
||||||
|
|
||||||
const repeat = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
expect(repeat.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(bg.openPopup).toHaveBeenCalledTimes(2);
|
|
||||||
expect(bg.actionPopups[bg.actionPopups.length - 1]).toBe(
|
|
||||||
"src/popup/index.html?approval=" + id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a prompt in a window is not opened again when the site asks again", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
// The browser will not open the toolbar popup, so the prompt goes to a
|
|
||||||
// window of its own.
|
|
||||||
bg.openPopup.mockImplementation(() =>
|
|
||||||
Promise.reject(new Error("no toolbar popup")),
|
|
||||||
);
|
|
||||||
bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
expect(bg.created).toHaveLength(1);
|
|
||||||
|
|
||||||
await rejectSignatureFromAnotherSite(bg);
|
|
||||||
expect(bg.created).toHaveLength(2);
|
|
||||||
|
|
||||||
const repeat = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
expect(repeat.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(bg.openPopup).toHaveBeenCalledTimes(1);
|
|
||||||
expect(bg.created).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a prompt in a connected toolbar popup is not opened again when the site asks again", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
const first = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
// The popup is open and showing the prompt.
|
|
||||||
bg.connectApproval(first.id());
|
|
||||||
|
|
||||||
await rejectSignatureFromAnotherSite(bg);
|
|
||||||
|
|
||||||
const repeat = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
expect(repeat.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(bg.openPopup).toHaveBeenCalledTimes(1);
|
|
||||||
expect(bg.actionPopups[bg.actionPopups.length - 1]).toBe(
|
|
||||||
"src/popup/index.html",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("another site's connection request is not held up", async () => {
|
|
||||||
const bg = loadBackground();
|
|
||||||
bg.requestSite();
|
|
||||||
const repeat = bg.requestSite();
|
|
||||||
const other = bg.requestSite(UNCONNECTED_ORIGIN);
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(repeat.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(other.result()).toBeNull();
|
|
||||||
expect(bg.created).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("another site's signature request is not held up", async () => {
|
|
||||||
const bg = loadBackground();
|
|
||||||
// Connect a second site, so that it may ask for a signature at all.
|
|
||||||
const connecting = bg.requestSite();
|
|
||||||
await settle();
|
|
||||||
const port = bg.connectApproval(connecting.id());
|
|
||||||
port.decide(true, false);
|
|
||||||
port.disconnect();
|
|
||||||
await settle();
|
|
||||||
expect(connecting.result()).toEqual({ result: [signer.address] });
|
|
||||||
|
|
||||||
bg.requestSign();
|
|
||||||
const repeat = bg.requestSign();
|
|
||||||
const other = bg.requestSign(signer.address, FRESH_ORIGIN);
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
expect(repeat.result()).toEqual(PENDING_REFUSAL);
|
|
||||||
expect(other.result()).toBeNull();
|
|
||||||
expect(bg.created).toHaveLength(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// A nonce collision found before the transaction reaches the network is the
|
// A nonce collision found before the transaction reaches the network is the
|
||||||
// one send failure the wallet can speak about with certainty. The user is told
|
// one send failure the wallet can speak about with certainty. The user is told
|
||||||
// it did not go out and to send it again, rather than being warned it might
|
// it did not go out and to send it again, rather than being warned it might
|
||||||
|
|||||||
@@ -3232,47 +3232,6 @@ test("personal_sign rejected returns a rejection to the page (#183)", async (env
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// A right-to-left character must not move the characters around it: U+05C3
|
|
||||||
// between "5" and "00" would otherwise put "500" on screen before it
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/403).
|
|
||||||
test("a personal message is laid out in the order of its bytes (#403)", async (env) => {
|
|
||||||
const text = "Pay 5" + String.fromCodePoint(0x05c3) + "00 ETH";
|
|
||||||
await startRequest(env.dapp, "sign-bidi", "personal_sign", [
|
|
||||||
hexlify(toUtf8Bytes(text)),
|
|
||||||
env.expectedAddress,
|
|
||||||
]);
|
|
||||||
const popup = await waitForApprovalWindow(env.ctx);
|
|
||||||
await visible(popup, "#view-approve-sign");
|
|
||||||
|
|
||||||
// The left edge of each character on screen, in byte order. A character
|
|
||||||
// the browser's fonts draw with no width shares its neighbour's edge.
|
|
||||||
const lefts = await popup.evaluate(() => {
|
|
||||||
const message = document.getElementById("approve-sign-message");
|
|
||||||
const textNode = message.firstChild;
|
|
||||||
const range = document.createRange();
|
|
||||||
const out = [];
|
|
||||||
for (let i = 0; i < textNode.length; i++) {
|
|
||||||
range.setStart(textNode, i);
|
|
||||||
range.setEnd(textNode, i + 1);
|
|
||||||
out.push(range.getBoundingClientRect().left);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
await clickAndClose(popup, "#btn-reject-sign");
|
|
||||||
await assertUserRejection(
|
|
||||||
env.dapp,
|
|
||||||
"sign-bidi",
|
|
||||||
"the byte-order personal_sign rejection",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lefts.length === text.length &&
|
|
||||||
lefts.every((left, i) => i === 0 || left >= lefts[i - 1]),
|
|
||||||
"the personal message is not laid out in byte order: " +
|
|
||||||
JSON.stringify(lefts),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("eth_signTypedData_v4 signs, and the signature recovers (#183)", async (env) => {
|
test("eth_signTypedData_v4 signs, and the signature recovers (#183)", async (env) => {
|
||||||
await startRequest(env.dapp, "typed", "eth_signTypedData_v4", [
|
await startRequest(env.dapp, "typed", "eth_signTypedData_v4", [
|
||||||
env.expectedAddress,
|
env.expectedAddress,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
// The signature prompt shows a personal message as the bytes that are signed
|
// The signature prompt shows a personal message as the bytes that are signed
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/403): the raw data in hex, the
|
// (https://git.eeqj.de/sneak/AutistMask/issues/403): the raw data in hex, the
|
||||||
// text it decodes to with control characters and characters that paint
|
// text it decodes to with control, zero-width and bidirectional characters
|
||||||
// nothing marked rather than obeyed, laid out in byte order, and a message
|
// marked rather than obeyed, and a message that is not hex as plain text that
|
||||||
// that is not hex as plain text that cannot be signed.
|
// cannot be signed.
|
||||||
//
|
//
|
||||||
// Driven against a minimal DOM stub in the shape
|
// Driven against a minimal DOM stub in the shape
|
||||||
// tests/approvalOrigin.test.js uses. That the layout keeps right-to-left
|
// tests/approvalOrigin.test.js uses.
|
||||||
// characters in byte order needs a real browser: tests/e2e/run.js checks it.
|
|
||||||
|
|
||||||
globalThis.chrome = {
|
globalThis.chrome = {
|
||||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
@@ -22,9 +21,6 @@ const FROM = "0x0000000000000000000000000000000000000a11";
|
|||||||
const RIGHT_TO_LEFT_OVERRIDE = String.fromCodePoint(0x202e);
|
const RIGHT_TO_LEFT_OVERRIDE = String.fromCodePoint(0x202e);
|
||||||
const POP_DIRECTIONAL_FORMATTING = String.fromCodePoint(0x202c);
|
const POP_DIRECTIONAL_FORMATTING = String.fromCodePoint(0x202c);
|
||||||
const ZERO_WIDTH_SPACE = String.fromCodePoint(0x200b);
|
const ZERO_WIDTH_SPACE = String.fromCodePoint(0x200b);
|
||||||
const VARIATION_SELECTOR_1 = String.fromCodePoint(0xfe00);
|
|
||||||
const VARIATION_SELECTOR_17 = String.fromCodePoint(0xe0100);
|
|
||||||
const HANGUL_FILLER = String.fromCodePoint(0x3164);
|
|
||||||
|
|
||||||
function makeElement(id) {
|
function makeElement(id) {
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
@@ -131,28 +127,6 @@ test("a zero-width character is marked", async () => {
|
|||||||
expect(shownMessage()).toBe("payU+200Bpal.com");
|
expect(shownMessage()).toBe("payU+200Bpal.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("variation selectors and a Hangul filler are marked", async () => {
|
|
||||||
// Each paints nothing, so a page could hide bytes after "Sign in".
|
|
||||||
await openPersonalSign(
|
|
||||||
hexlify(
|
|
||||||
toUtf8Bytes(
|
|
||||||
"Sign in" +
|
|
||||||
VARIATION_SELECTOR_1 +
|
|
||||||
VARIATION_SELECTOR_17 +
|
|
||||||
HANGUL_FILLER,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(shownMessage()).toBe("Sign inU+FE00U+E0100U+3164");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the message is laid out in byte order", async () => {
|
|
||||||
await openPersonalSign(hexlify(toUtf8Bytes("Hello")));
|
|
||||||
expect(
|
|
||||||
node("approve-sign-message").classList.contains("am-byte-order"),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a line feed is shown as a line break", async () => {
|
test("a line feed is shown as a line break", async () => {
|
||||||
await openPersonalSign(hexlify(toUtf8Bytes("Sign in\nNonce: 7")));
|
await openPersonalSign(hexlify(toUtf8Bytes("Sign in\nNonce: 7")));
|
||||||
expect(node("approve-sign-message").innerHTML).toBe("Sign in<br>Nonce: 7");
|
expect(node("approve-sign-message").innerHTML).toBe("Sign in<br>Nonce: 7");
|
||||||
@@ -167,13 +141,6 @@ test("the raw hex is shown alongside the text", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hex with an uppercase 0X is read as hex, as signing reads it", async () => {
|
|
||||||
await openPersonalSign("0X48656C6C6F");
|
|
||||||
expect(shownMessage()).toBe("Hello");
|
|
||||||
expect(node("approve-sign-hex").textContent).toBe("0X48656C6C6F");
|
|
||||||
expect(node("btn-approve-sign").disabled).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("bytes that are not text are shown only as hex", async () => {
|
test("bytes that are not text are shown only as hex", async () => {
|
||||||
await openPersonalSign("0xff00");
|
await openPersonalSign("0xff00");
|
||||||
expect(node("approve-sign-message").textContent).toBe(
|
expect(node("approve-sign-message").textContent).toBe(
|
||||||
|
|||||||
Reference in New Issue
Block a user