Compare commits

..
Author SHA1 Message Date
sneak 60d2b24cd1 security: announce a fresh EIP-6963 provider UUID per page load (closes #398)
e2e / e2e-chrome (push) Failing after 0s
e2e / e2e-firefox (push) Failing after 0s
check / check (push) Successful in 44s
The provider UUID was generated once, stored in extension storage, and
announced verbatim to every page on every load and across restarts, so any
site — connected or not — could read a stable cross-site, cross-session
identifier for the install: a supercookie contradicting the "no tracking"
promise. EIP-6963 wants a fresh UUIDv4 per announcement instead.

inpage.js now announces a per-load crypto.randomUUID() and persists nothing;
the eip6963Uuid storage key and the AUTISTMASK_PROVIDER_UUID content-script
message are removed. That key was a standalone top-level storage entry, never
part of the versioned autistmask profile, so stateSchema.js and the
persisted-field harness are untouched and no existing profile is affected.

A jest test asserts two loads announce different UUIDv4s and that one load
reuses a single UUID across re-announcements.

Model: opus-4-8
2026-09-21 23:35:57 +00:00
clawbot 598de3ff1a fix: re-enable Confirm Delete after a delete, so a second one needs no reopen (closes #335)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s
The password route disabled its Confirm Delete button before the decrypt
and never re-enabled it on success, so a second delete in the same popup
session found a dead button until the popup was closed and reopened. The
lost-password route re-enabled its own button in its leave hook, so the
two screens on the one screen behaved differently.

Both routes now reset the button through the shared finishDelete(), the
one path they both take, and the lost-password leave hook no longer
handles it separately. Tests drive a password-route delete and a second
delete in the same session; they fail against the prior head, where the
button stays disabled after the first delete.

Model: opus-4-8
2026-09-22 01:28:02 +02:00
5 changed files with 127 additions and 42 deletions
+11
View File
@@ -143,6 +143,17 @@ but the review is broader than any of them.
both routes take, so they behave the same and the button is live for the next
delete.
- 2026-09-21: The EIP-6963 provider UUID is generated fresh on each page load
and never persisted ([#398](https://git.eeqj.de/sneak/AutistMask/issues/398)).
It was created once and stored, then announced verbatim to every page on every
load and across restarts, so any site — connected or not — could read it as a
stable cross-site, cross-session identifier for the install, contradicting the
"no tracking" promise. inpage.js now announces a per-load
`crypto.randomUUID()` and the `eip6963Uuid` storage key and the
`AUTISTMASK_PROVIDER_UUID` content-script message are gone. That key was a
standalone storage entry, never part of the versioned `autistmask` profile, so
the state schema is untouched and no existing profile is affected.
- 2026-08-30: An address no longer wraps, or is shortened to fit, in any of the
common views ([#380](https://git.eeqj.de/sneak/AutistMask/issues/380)). The
wallet list was the reported case: the address shared one row with the
-26
View File
@@ -5,8 +5,6 @@ const {
hasBrowserNamespace,
runtimeApi,
sendMessage,
storageGet,
storageSet,
} = require("../shared/browserApi");
// In Chrome (MV3), inpage.js runs as a MAIN-world content script declared
@@ -21,30 +19,6 @@ if (hasBrowserNamespace()) {
(document.head || document.documentElement).appendChild(script);
}
// Send the persisted EIP-6963 provider UUID to the inpage script.
// Generated once at install time and stored in extension storage.
(async function sendProviderUuid() {
let uuid = null;
try {
const items = await storageGet("eip6963Uuid");
uuid = items?.eip6963Uuid;
if (!uuid) {
uuid = crypto.randomUUID();
await storageSet({ eip6963Uuid: uuid });
}
} catch {
// Storage was unavailable or refused the write. The announcement
// still has to go out — a provider that never announces is invisible
// to every EIP-6963 dApp — so it goes under a fresh uuid that this
// page load will not outlive.
if (!uuid) uuid = crypto.randomUUID();
}
window.postMessage(
{ type: "AUTISTMASK_PROVIDER_UUID", uuid },
location.origin,
);
})();
// Relay requests from the page to the background script
window.addEventListener("message", (event) => {
if (event.source !== window) return;
+8 -11
View File
@@ -204,7 +204,14 @@
"</svg>",
);
let providerUuid = crypto.randomUUID(); // fallback until real UUID arrives
// EIP-6963 wants a fresh UUIDv4 per page load — it identifies one
// announcement, so a provider can be told apart from another instance of
// itself in the same page. It is generated here and never stored:
// announcing one persisted value to every site, on every load and across
// restarts, turned it into a stable cross-site, cross-session tracking
// identifier any page could read
// (https://git.eeqj.de/sneak/AutistMask/issues/398).
const providerUuid = crypto.randomUUID();
function buildProviderInfo() {
return {
@@ -226,16 +233,6 @@
);
}
// Listen for the persisted UUID from the content script
function onProviderUuid(event) {
if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return;
window.removeEventListener("message", onProviderUuid);
providerUuid = event.data.uuid;
announceProvider();
}
window.addEventListener("message", onProviderUuid);
window.addEventListener("eip6963:requestProvider", announceProvider);
announceProvider();
+5 -5
View File
@@ -372,10 +372,10 @@ step("the loopback dApp page gets the real inpage provider", async (env) => {
STEP_TIMEOUT_MS,
);
// EIP-6963, asked of the provider itself. The announcement carries the
// uuid src/content/index.js reads out of extension storage — call site 1
// in the issue — and it has to name this extension and hand back the very
// object on window.ethereum.
// EIP-6963, asked of the provider itself. The announcement carries a
// UUIDv4 inpage.js generates fresh for this page load (nothing persists
// it — see issue #398) and has to name this extension and hand back the
// very object on window.ethereum.
const announced = await d.executeAsync(
`const done = arguments[arguments.length - 1];
const onAnnounce = (e) => {
@@ -402,7 +402,7 @@ step("the loopback dApp page gets the real inpage provider", async (env) => {
);
assert(
typeof announced.uuid === "string" && announced.uuid.length === 36,
"the announcement carries no stored provider uuid: " +
"the announcement carries no provider uuid: " +
JSON.stringify(announced.uuid),
);
+103
View File
@@ -0,0 +1,103 @@
// The EIP-6963 provider UUID inpage.js announces (src/content/inpage.js).
//
// The bug this pins down (issue #398): the UUID used to be generated once,
// persisted in extension storage, and announced verbatim to every page on
// every load and across browser restarts, so any site — connected or not —
// could read a stable cross-site, cross-session identifier for the install.
// EIP-6963 wants a fresh UUIDv4 per announcement instead. The fix generates
// it per page load and stores nothing.
//
// inpage.js is a bare IIFE injected into the page's JS context, not a module;
// see tests/inpageErrors.test.js for why it is evaluated against a stub window
// rather than imported. Here the stub captures the CustomEvent that carries
// the announcement, so the UUID this file reads is the one a real dApp's
// eip6963:announceProvider listener would see.
const fs = require("fs");
const path = require("path");
const { webcrypto } = require("crypto");
const SOURCE = fs.readFileSync(
path.join(__dirname, "..", "src", "content", "inpage.js"),
"utf8",
);
const loadInto = new Function(
"window",
"self",
"crypto",
"Event",
"CustomEvent",
SOURCE,
);
class StubEvent {
constructor(type) {
this.type = type;
}
}
class StubCustomEvent extends StubEvent {
constructor(type, init) {
super(type);
this.detail = init && init.detail;
}
}
// Evaluate inpage.js once against a fresh stub window and return every UUID it
// announced. A `requestProvider` event is dispatched too, so a re-announcement
// within one load is observed as well as the announcement at load.
function announcedUuids() {
const listeners = {};
const uuids = [];
const win = {
addEventListener(type, fn) {
(listeners[type] || (listeners[type] = [])).push(fn);
},
removeEventListener(type, fn) {
const fns = listeners[type];
if (!fns) return;
const i = fns.indexOf(fn);
if (i !== -1) fns.splice(i, 1);
},
postMessage() {},
dispatchEvent(event) {
if (event.type === "eip6963:announceProvider") {
uuids.push(event.detail.info.uuid);
}
for (const fn of (listeners[event.type] || []).slice()) fn(event);
return true;
},
};
win.window = win;
loadInto(win, win, webcrypto, StubEvent, StubCustomEvent);
win.dispatchEvent(new StubEvent("eip6963:requestProvider"));
return uuids;
}
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
describe("the EIP-6963 provider UUID is fresh per page load", () => {
test("a load announces a UUIDv4, unprompted, with nothing delivered", () => {
const uuids = announcedUuids();
expect(uuids.length).toBeGreaterThan(0);
expect(uuids[0]).toMatch(UUID_V4);
});
test("every announcement within one load carries the same UUID", () => {
const uuids = announcedUuids();
expect(uuids.length).toBeGreaterThan(1);
expect(new Set(uuids).size).toBe(1);
});
test("two page loads announce different UUIDs", () => {
const first = announcedUuids()[0];
const second = announcedUuids()[0];
expect(first).toMatch(UUID_V4);
expect(second).toMatch(UUID_V4);
expect(second).not.toBe(first);
});
});