Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70f0feee28 |
@@ -619,13 +619,14 @@ The jobs **report, they do not gate.** A failure is a red mark against the
|
||||
commit that a reviewer has to account for, not a hard block: whether a check
|
||||
blocks a merge is Gitea branch protection, which this repo does not configure.
|
||||
|
||||
That is not only a statement about configuration. The Chrome suite is
|
||||
**measurably flaky under load** — two of six runs of unmutated code on a busy
|
||||
machine lost the approval popup out from under the dApp signing wait, always in
|
||||
the `#183` section, tracked as
|
||||
[#287](https://git.eeqj.de/sneak/AutistMask/issues/287). So a red `e2e-chrome`
|
||||
has to be read before it is believed, and that flake is the blocker to ever
|
||||
making this a required check. Do not answer it with a retry wrapper: a suite
|
||||
That is not only a statement about configuration. Reports of the Chrome suite
|
||||
**failing under load** are still open, among them
|
||||
[#290](https://git.eeqj.de/sneak/AutistMask/issues/290), runs on a busy machine
|
||||
failing with `the extension opened no approval window within 30000ms`, and
|
||||
[#446](https://git.eeqj.de/sneak/AutistMask/issues/446), the Settings round trip
|
||||
closing the popup before its network switch is saved. So a red `e2e-chrome` has
|
||||
to be read before it is believed, and those failures are the blocker to ever
|
||||
making this a required check. Do not answer them with a retry wrapper: a suite
|
||||
that reruns until it is green stops being evidence.
|
||||
|
||||
Nothing in either job can pass vacuously. There is no `continue-on-error` and no
|
||||
@@ -1788,10 +1789,7 @@ view would leave a wallet one click from deletion.
|
||||
new password — and, in bold, that without that phrase written down the
|
||||
deletion loses everything the wallet holds, forever
|
||||
- That the other wallets are not touched
|
||||
- The wallet's name, and a text input asking for it to be typed back. A name
|
||||
that shows nothing at all (only spaces, or only characters that paint
|
||||
nothing) is shown as "Wallet N", its position in the list, and that is
|
||||
what is typed back.
|
||||
- The wallet's name, and a text input asking for it to be typed back
|
||||
- Error line
|
||||
- "Delete This Wallet Forever" button
|
||||
- **Transitions**:
|
||||
@@ -1799,9 +1797,9 @@ view would leave a wallet one click from deletion.
|
||||
outcomes as "Confirm Delete" above, through the same `finishDelete()`, so
|
||||
the selection repair, permission cleanup and `AUTISTMASK_ACTIVE_CHANGED`
|
||||
broadcast are identical on both routes
|
||||
- "Delete This Wallet Forever" (name does not match, or the field is empty)
|
||||
→ "That is not the name of this wallet. Type <name> to confirm." on
|
||||
the error line, nothing deleted
|
||||
- "Delete This Wallet Forever" (name does not match) → "That is not the name
|
||||
of this wallet. Type <name> to confirm." on the error line, nothing
|
||||
deleted
|
||||
- "Back" → **DeleteWallet**, re-entered through its `show()` so the wallet
|
||||
selection comes back with it. The two delete screens are siblings rather
|
||||
than parent and child: nothing is pushed on the way here, so both have
|
||||
@@ -1810,13 +1808,8 @@ view would leave a wallet one click from deletion.
|
||||
secret protects nobody: an attacker at the popup who wants the wallet gone can
|
||||
uninstall the extension, so the only person such a gate stops is the owner who
|
||||
forgot it. The typed name is a check that the user knows which wallet they are
|
||||
on, not a secret, so it is matched as the user can see it: letter case,
|
||||
surrounding spaces and repeated inner spaces are ignored, and characters that
|
||||
paint nothing (format characters such as the zero-width space,
|
||||
default-ignorable characters, and DELETE — the same set
|
||||
`src/shared/symbolSpoof.js` strips) are removed from both sides before
|
||||
comparing. An empty field, or one holding only spaces or such characters, is
|
||||
refused whatever the wallet is called.
|
||||
on, not a secret, so it is matched with surrounding spaces and letter case
|
||||
ignored.
|
||||
- Not in `RESTORABLE_VIEWS`, alongside `delete-wallet-confirm`: a popup reopened
|
||||
by accident must not land on a screen whose button erases key material.
|
||||
|
||||
|
||||
@@ -45,15 +45,17 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-05: The lost-password delete confirmation refuses an empty field and
|
||||
ignores characters that paint nothing
|
||||
([#336](https://git.eeqj.de/sneak/AutistMask/issues/336)). A wallet named only
|
||||
with spaces compared equal to an empty field, so typing nothing would have
|
||||
deleted it, and a zero-width space in a name made the name impossible to type
|
||||
back. An empty field is now refused whatever the name is, the same invisible
|
||||
characters `src/shared/symbolSpoof.js` strips are removed from both sides, and
|
||||
a name that shows nothing is shown and typed back as "Wallet N".
|
||||
|
||||
- 2026-10-05: The extension no longer opens a window for a site-connection
|
||||
prompt already answered
|
||||
([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). When the prompt was
|
||||
decided before the toolbar popup raised for it had loaded, that popup was torn
|
||||
down, `chrome.action.openPopup()` rejected, and the background opened its
|
||||
fallback window for the answered approval and then removed it. In the Chrome
|
||||
end-to-end suite the next test could take that window for its own prompt and
|
||||
lose it under its wait. `openApprovalWindow()` now opens nothing for an
|
||||
approval that is no longer pending. The blocklist test's Reject, whose window
|
||||
closes itself, is clicked as the other site Reject is, with the click
|
||||
witnessed.
|
||||
- 2026-10-05: A `holders_count` that is not a whole number in plain digits is
|
||||
unknown, not read in part
|
||||
([#251](https://git.eeqj.de/sneak/AutistMask/issues/251)). `parseInt` read
|
||||
|
||||
@@ -413,7 +413,7 @@ function releaseApproval(approval) {
|
||||
}
|
||||
}
|
||||
|
||||
// Open approval in a separate popup window.
|
||||
// Open approval in a separate popup window, unless it is no longer pending.
|
||||
// This is the primary mechanism for tx/sign approvals (triggered programmatically,
|
||||
// not from a user gesture) and the fallback for site-connection approvals.
|
||||
// Never rejects. Its callers raise it from inside a Promise executor and drop
|
||||
@@ -446,6 +446,10 @@ async function openApprovalWindow(id) {
|
||||
);
|
||||
}
|
||||
|
||||
// Already answered: a site-connection prompt decided before the toolbar
|
||||
// popup raised for it had loaded, whose openPopup() rejects only now.
|
||||
if (!pendingApprovals[id]) return;
|
||||
|
||||
let win = null;
|
||||
try {
|
||||
win = await windowsCreate(opts);
|
||||
|
||||
@@ -12,7 +12,6 @@ const {
|
||||
removeWalletFromState,
|
||||
broadcastActiveChanged,
|
||||
} = require("../../shared/walletDelete");
|
||||
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
||||
|
||||
let deleteWalletIndex = null;
|
||||
let lostPasswordIndex = null;
|
||||
@@ -21,31 +20,21 @@ let ctx = null;
|
||||
// The name shown for a wallet, and on the lost-password screen the string
|
||||
// the user has to type back. One function so the two cannot disagree: a
|
||||
// confirmation that asks for a name other than the one on screen is
|
||||
// unusable. A name that shows nothing at all (only spaces, or only
|
||||
// zero-width characters) is replaced by "Wallet N" for the same reason:
|
||||
// there would be nothing on screen to type back.
|
||||
// unusable.
|
||||
function displayName(walletIdx) {
|
||||
const wallet = state.wallets[walletIdx];
|
||||
const name = wallet && wallet.name;
|
||||
if (name && confirmKey(name)) return name;
|
||||
return "Wallet " + (walletIdx + 1);
|
||||
return (wallet && wallet.name) || "Wallet " + (walletIdx + 1);
|
||||
}
|
||||
|
||||
// What the typed confirmation and the wallet name are compared as. HTML
|
||||
// collapses runs of whitespace when it renders the name, so a wallet named
|
||||
// "My Wallet" with two spaces DISPLAYS as "My Wallet": the user cannot
|
||||
// see the second space and cannot type a string that matches the stored
|
||||
// name. Characters that paint nothing, such as a zero-width space, are
|
||||
// invisible the same way and are removed first. Comparing this form on
|
||||
// both sides is what keeps the confirmation satisfiable, on the one screen
|
||||
// whose whole purpose is unwedging a user who is already stuck. Case and
|
||||
// surrounding space go the same way.
|
||||
// name. Comparing collapsed on both sides is what keeps the confirmation
|
||||
// satisfiable, on the one screen whose whole purpose is unwedging a user
|
||||
// who is already stuck. Case and surrounding space go the same way.
|
||||
function confirmKey(name) {
|
||||
return name
|
||||
.replace(INVISIBLE_CHARACTERS, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.toLowerCase();
|
||||
return name.trim().replace(/\s+/g, " ").toLowerCase();
|
||||
}
|
||||
|
||||
// Drop the password from the DOM and the wallet selection from the
|
||||
@@ -185,16 +174,14 @@ function init(_ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Case, surrounding spaces, repeated inner spaces and invisible
|
||||
// characters are not part of the confirmation; see confirmKey().
|
||||
// This asks whether the user knows which wallet they are on; it is
|
||||
// not a secret, and refusing "wallet 2" for "Wallet 2" would only
|
||||
// teach the user to distrust the control. An empty field is
|
||||
// refused whatever the wallet is called, so no stored name can
|
||||
// ever be confirmed by typing nothing.
|
||||
const typed = confirmKey($("delete-wallet-lost-name-input").value);
|
||||
// Case, surrounding spaces and repeated inner spaces are not part
|
||||
// of the confirmation; see confirmKey(). This asks whether the
|
||||
// user knows which wallet they are on; it is not a secret, and
|
||||
// refusing "wallet 2" for "Wallet 2" would only teach the user to
|
||||
// distrust the control.
|
||||
const typed = $("delete-wallet-lost-name-input").value;
|
||||
const expected = displayName(lostPasswordIndex);
|
||||
if (typed === "" || typed !== confirmKey(expected)) {
|
||||
if (confirmKey(typed) !== confirmKey(expected)) {
|
||||
$("delete-wallet-lost-flash").textContent =
|
||||
"That is not the name of this wallet. Type " +
|
||||
expected +
|
||||
|
||||
@@ -2235,6 +2235,27 @@ describe("a site connection decided as the popup closes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The prompt is decided before the toolbar popup raised for it has
|
||||
// loaded; that popup is torn down and openPopup() rejects only after.
|
||||
test("a toolbar prompt already decided opens no window when openPopup() rejects", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
const opening = deferred();
|
||||
bg.openPopup.mockImplementation(() => opening.promise);
|
||||
const pending = bg.requestSite();
|
||||
await settle();
|
||||
|
||||
const port = bg.connectApproval(pending.id());
|
||||
port.decide(true, false);
|
||||
port.disconnect();
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: [signer.address] });
|
||||
|
||||
opening.reject(new Error("the toolbar popup closed before it loaded"));
|
||||
await settle();
|
||||
|
||||
expect(bg.created).toHaveLength(0);
|
||||
});
|
||||
|
||||
// The port carries a decision now, so it carries the sender check the
|
||||
// one-off message used to carry. A content script that guessed an
|
||||
// approval id must not be able to connect the site it is running on.
|
||||
|
||||
@@ -46,9 +46,6 @@ const A1 = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
const B0 = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599";
|
||||
const C0 = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||
|
||||
// U+200B, built from its code point so that it can be seen in this file.
|
||||
const ZERO_WIDTH_SPACE = String.fromCodePoint(0x200b);
|
||||
|
||||
// ------------------------------------------------------------ DOM stub
|
||||
|
||||
function makeElement(id) {
|
||||
@@ -345,72 +342,6 @@ describe("the typed confirmation", () => {
|
||||
"secret-three",
|
||||
]);
|
||||
});
|
||||
|
||||
// A name of only spaces compares as nothing, and so does an empty
|
||||
// field. Typing nothing must still delete nothing.
|
||||
test.each(["", " "])(
|
||||
"typing %j deletes nothing when the name is only spaces",
|
||||
async (typedValue) => {
|
||||
const { deleteWallet, state, storage } = load();
|
||||
state.wallets[1].name = " ";
|
||||
await openLostPassword(deleteWallet, 1);
|
||||
|
||||
node("delete-wallet-lost-name-input").value = typedValue;
|
||||
await click("btn-delete-wallet-lost-confirm");
|
||||
|
||||
expect(node("delete-wallet-lost-flash").style.visibility).toBe(
|
||||
"visible",
|
||||
);
|
||||
expect(state.wallets).toHaveLength(3);
|
||||
expect(await persistedWallets(storage)).toHaveLength(3);
|
||||
},
|
||||
);
|
||||
|
||||
// A name that shows nothing would leave nothing on screen to type
|
||||
// back, so the screen names the wallet by its position instead, and
|
||||
// that is what the user types.
|
||||
test.each([
|
||||
["spaces", " "],
|
||||
["a zero-width space", ZERO_WIDTH_SPACE],
|
||||
])(
|
||||
"a name of only %s is shown and typed back as Wallet 2",
|
||||
async (_label, storedName) => {
|
||||
const { deleteWallet, state, storage } = load();
|
||||
state.wallets[1].name = storedName;
|
||||
await openLostPassword(deleteWallet, 1);
|
||||
|
||||
expect(node("delete-wallet-lost-name").textContent).toBe(
|
||||
"Wallet 2",
|
||||
);
|
||||
node("delete-wallet-lost-name-input").value = "Wallet 2";
|
||||
await click("btn-delete-wallet-lost-confirm");
|
||||
|
||||
const persisted = await persistedWallets(storage);
|
||||
expect(persisted.map((w) => w.encryptedSecret)).toEqual([
|
||||
"secret-one",
|
||||
"secret-three",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
// A zero-width space paints nothing, so "My", a zero-width space and
|
||||
// "Wallet" reads as "MyWallet", and that is all the user can type. HTML
|
||||
// does not collapse it the way it collapses spaces, so it has to be
|
||||
// removed explicitly.
|
||||
test("a zero-width space inside the name is not part of it", async () => {
|
||||
const { deleteWallet, state, storage } = load();
|
||||
state.wallets[1].name = "My" + ZERO_WIDTH_SPACE + "Wallet";
|
||||
await openLostPassword(deleteWallet, 1);
|
||||
|
||||
node("delete-wallet-lost-name-input").value = "MyWallet";
|
||||
await click("btn-delete-wallet-lost-confirm");
|
||||
|
||||
const persisted = await persistedWallets(storage);
|
||||
expect(persisted.map((w) => w.encryptedSecret)).toEqual([
|
||||
"secret-one",
|
||||
"secret-three",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleting without the password", () => {
|
||||
|
||||
+4
-2
@@ -2738,7 +2738,7 @@ async function closeApprovalPages(ctx) {
|
||||
// #btn-reject on the site prompt — NOT self-proving. A page that went away
|
||||
// without the click landing disconnects the approval port, the background
|
||||
// settles that as 4001, and 4001 is exactly what assertUserRejection
|
||||
// accepts. That call site arms the click trace below and asserts it.
|
||||
// accepts. Both call sites arm the click trace below and assert it.
|
||||
//
|
||||
// A button that is missing or unclickable raises a different error, which is
|
||||
// rethrown.
|
||||
@@ -3124,7 +3124,9 @@ test("a connect request from a blocklisted site is flagged (#219)", async (env)
|
||||
// Not remembered: a remembered decision for this origin would
|
||||
// outlive the test.
|
||||
await popup.uncheck("#approve-remember");
|
||||
await popup.click("#btn-reject");
|
||||
await armClickTrace(env, popup, "#btn-reject");
|
||||
await clickAndClose(popup, "#btn-reject");
|
||||
await assertClickLanded(env, "#btn-reject");
|
||||
|
||||
await assertUserRejection(
|
||||
phishingDapp,
|
||||
|
||||
Reference in New Issue
Block a user