diff --git a/TODO.md b/TODO.md index 8df4a10..9832711 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/src/content/index.js b/src/content/index.js index 489848a..4c71f72 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -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; diff --git a/src/content/inpage.js b/src/content/inpage.js index 33903e5..d107326 100644 --- a/src/content/inpage.js +++ b/src/content/inpage.js @@ -204,7 +204,14 @@ "", ); - 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(); diff --git a/tests/e2e/firefox/run.js b/tests/e2e/firefox/run.js index 8693bab..2fa91f7 100644 --- a/tests/e2e/firefox/run.js +++ b/tests/e2e/firefox/run.js @@ -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), ); diff --git a/tests/inpageProviderUuid.test.js b/tests/inpageProviderUuid.test.js new file mode 100644 index 0000000..cd747ab --- /dev/null +++ b/tests/inpageProviderUuid.test.js @@ -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); + }); +});