// The extension identity on both browsers, pinned. // // This is the anti-regression check for // https://git.eeqj.de/sneak/AutistMask/issues/310. An unpacked Chrome // extension with no `key` in its manifest gets an id derived from the // ABSOLUTE PATH it was loaded from, and chrome.storage.local is partitioned by // that id. Move the checkout, re-clone it, or load it from a second directory, // and the wallet is silently gone: the extension comes up on a fresh, empty // storage partition with no error anywhere. `key` pins the id to the public // key instead of to the path, which is what makes the storage survive. // // So the id is asserted as a literal. A test that merely recomputed the id // from whatever `key` happened to be in the manifest would pass after someone // replaced the key — and replacing the key is exactly the change that orphans // every existing wallet. The value below is the promise; changing it is a // migration, not an edit. // // Firefox needs no key: browser_specific_settings.gecko.id declares the id // directly, and it is pinned here for the same reason. The Firefox e2e suite // depends on it too (tests/e2e/firefox/driver.js maps it to a fixed uuid), and // tests/e2e/firefox/reinstall.js is the empirical half — it removes the add-on // and installs it again and reads the vault back out. const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const MANIFEST_DIR = path.join(__dirname, "..", "manifest"); // The public half of an RSA keypair, DER-encoded SubjectPublicKeyInfo, base64. // The PRIVATE half is not in this repo and is not needed to build, load or // test anything here: it is only ever used to sign a CRX, which this repo does // not do. const CHROME_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzy/G9gT4Z3Ci0HCmthUPEiCjENg+" + "5meZpjdogyT7SiMfxENtHdrpDL6wGhAg1Dk0f1C67Ft8OYpMrMH3kiP2Wnt0UpHo45PY0YUU" + "YzdJgbsp8u0kaykd5FFiY6FycIIFaTniMuh7wRKuNNdJWly+H3aG7qZ6nGu5PIMdb1GXUk35" + "hY+yl7dz5dqFFYUCyxvWCT9XGBSYiI+XRBB/rVZjMWfWpaTmRPdOZ4+GO/Lx0OdMxKlPA/kL" + "WoPot5vMlLn2FDPu6sASphiu7dKZnrINW+h/27jlHMJQS0jncB1EgqOHW0vbXrZnTveFX6UW" + "+Qp86FfSkikhKtQgTW2A4mtWawIDAQAB"; // chrome.storage.local for this extension lives under this id, and nowhere // else. const CHROME_EXTENSION_ID = "gipbhkogfopeahplcjhipkgpcimdpkip"; const FIREFOX_EXTENSION_ID = "autistmask@sneak.berlin"; // Chrome's id derivation: sha256 of the DER public key, first 16 bytes, each // hex digit mapped 0-f onto a-p. Written out here rather than taken on trust, // because the whole claim of this file is that the committed key produces that // id. function chromeExtensionId(keyBase64) { const der = Buffer.from(keyBase64, "base64"); const digest = crypto.createHash("sha256").update(der).digest("hex"); return [...digest.slice(0, 32)] .map((c) => String.fromCharCode(97 + parseInt(c, 16))) .join(""); } function readManifest(name) { return JSON.parse( fs.readFileSync(path.join(MANIFEST_DIR, name + ".json"), "utf8"), ); } describe("chrome extension identity", () => { test("the manifest carries the pinned key", () => { expect(readManifest("chrome").key).toBe(CHROME_KEY); }); test("the key is a well-formed RSA public key", () => { const der = Buffer.from(CHROME_KEY, "base64"); // Round-trips: a truncated or re-wrapped base64 blob would still // decode to bytes, and Chrome would then derive an id from garbage. expect(der.toString("base64")).toBe(CHROME_KEY); const key = crypto.createPublicKey({ key: der, format: "der", type: "spki", }); expect(key.asymmetricKeyType).toBe("rsa"); expect(key.asymmetricKeyDetails.modulusLength).toBe(2048); }); test("the key derives the pinned extension id", () => { expect(chromeExtensionId(CHROME_KEY)).toBe(CHROME_EXTENSION_ID); expect(CHROME_EXTENSION_ID).toMatch(/^[a-p]{32}$/); }); // The private half is a credential. It has never been in this repo and no // target generates one into the working tree; this fails loudly if that // ever changes, because a committed .pem is a key anyone can sign a CRX // with under this extension's id. test("no private key is committed anywhere in the tree", () => { const tracked = require("child_process") .execSync("git ls-files", { cwd: path.join(__dirname, ".."), encoding: "utf8", }) .split("\n") .filter(Boolean); expect(tracked.filter((f) => /\.(pem|key|p12|pfx)$/i.test(f))).toEqual( [], ); }); }); describe("firefox extension identity", () => { test("the manifest declares the pinned gecko id", () => { const gecko = readManifest("firefox").browser_specific_settings.gecko; expect(gecko.id).toBe(FIREFOX_EXTENSION_ID); }); // Firefox derives nothing from the path, so no key field belongs here; one // would be ignored and would only suggest the id came from somewhere else. test("the firefox manifest carries no chrome key field", () => { expect(readManifest("firefox").key).toBeUndefined(); }); });