// 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();
    });
});
