# Every blocked extension, recovered — 129 of 131 CRX bundles

**Goal.** The school's Chrome policy removed 131 extensions by ID. An extension ID is also the token
the browser uses to ask Google for updates, so most of those "removed" extensions are still
downloadable binaries. Fetch them all, verify them, and publish the evidence — while being explicit
about the two that no longer exist anywhere.

Companion project: **Island College Chrome policy — Audit** (`/analysis/island-chrome-policy/`) decoded
the policy these IDs came from. This project asks a different question — *can the blocked things still
be obtained?* — and is a separate card, page and artefact. Not merged, cross-linked both ways.

## Constraints

- Read-only with respect to the managed profile: no account, no cookie, no policy change, no attempt to
  make anything installable on a school machine. The deliverable is an archive + a report.
- No API key, no login. The Web Store CRX update service is open; that is the whole method.
- Every number in the report is computed from the capture manifest at build time, never typed.
- The page must be self-contained (no external CSS/JS/fonts) so it prints identically to PDF.
- Never round 129 up to 131. Non-recovery is stated in the deck, a stat tile, a section, the table and
  the footer.

## Method

1. Parse the 131 IDs out of the cached `ExtensionSettings` policy (see the companion prompt for the
   protobuf decode). Keep the policy's own category groups.
2. Ask the update service, once per ID, as a macOS arm64 Chrome 126 client:

   ```
   https://clients2.google.com/service/update2/crx?response=redirect
     &os=mac&arch=arm64&os_arch=arm64&nacl_arch=arm64
     &prod=chromiumcrx&prodchannel=unknown&prodversion=126.0.0.0
     &acceptformat=crx2,crx3&x=id%3D<ID>%26uc
   ```

   8 workers, 4 retries with backoff, 90 s timeout.
3. Classify the reply: `204` / 0 bytes = **delisted** (terminal — do not retry, do not report as a
   network error). `200` with `Cr24` magic = a real CRX3.
4. Validate three ways: magic bytes `43 72 32 34`, header version 3, then strip `12 + header_length`
   and run `zipfile.testzip()` over the payload — a full CRC sweep, not a header peek. Parse the
   version out of the redirect filename (`/ext_126.0.0.0_<version>.crx`) and cross-check it against
   the `manifest.json` inside the archive.
5. SHA-256 the delivered bytes (signature header included) into `crx-manifest.json`.
6. For anything that 204s, probe **every** mirror before declaring it gone: legacy endpoint without
   `acceptformat`, crx4chrome, crxsoso, extpose, chrome-stats, Wayback CDX + availability API. Record
   the result of each route in the report's evidence table.

## Numbers this run produced

| | |
|---|---|
| Listed | 131 |
| Recovered | 129 (98.5%) |
| Bytes | 1,567,361,468 (1.46 GiB) |
| Files if unpacked | 35,269 (2.3 GiB) |
| Corrupt | 0 — 129/129 pass the CRC sweep |
| Over 25 MiB host cap | 19 files, 1,034 MiB |
| Under the cap | 110 files, 483 MB — still 10× the rest of the site |
| Delisted | 2 — Gauth, ZenMate |

## Design decisions

- **Ship folders, not just `.crx`.** Isaac's correction, and it is the right one: a `.crx` is a zip
  with a CRX3 signature prefix, so a normal person who downloads one gets an "the file may be damaged"
  dialog and no obvious next step. So each extension is published as `<Name>__<id>.zip` whose **single
  top-level entry is the extension's directory** — unzip once, get a folder with `manifest.json` at its
  root, then `chrome://extensions` → Developer mode → Load unpacked. Keep the untouched `.crx` beside it
  for anyone who prefers drag-install.
- **Verify the folder with a tree hash.** A directory has no SHA-256, so record
  `sha256(sorted "relpath\0filehash\n")` per extension. Proof it works: a downloaded 1,578-file folder
  re-hashes to the recorded value after round-tripping through the release.
- **The binaries are not served from this domain — they are published elsewhere.** 19 of 129 exceed
  the 25 MiB single-file limit of a static host, and the uploadable remainder is 483 MB across ~20,000
  files. So the archive lives on a **GitHub release** (`testisaac1804-byte/crx-archive`, tag `v1.0`):
  258 assets (129 folders + 129 bundles), 2 GiB per asset, no account to download, CDN-served, and it
  does not depend on this Mac being awake. The page stays the index: every one of the 131 rows carries
  its own folder link plus `.crx`, there is one button for the whole set, and `downloads.json` maps
  ID → zip → crx → digests so a script can mirror the lot.
- **Verify downloads against the server's digest, not your own arithmetic.** After upload, pull
  `releases/tags/v1.0` from the API and compare each asset's `digest` with the locally computed
  SHA-256: 258/258 matched. That is the end-to-end proof that what a stranger downloads is
  byte-identical to what was captured.
- **Two stat tiles carry the bad news** (`2 delisted`, and later `19 over cap`) so the gap is visible
  before the reader scrolls.
- Section order: two-projects table → the protocol → what came back → **the full 131-row list** →
  the two that don't exist → integrity → why not hosted → reproduce.

## Traps

- **`204` is not an error.** urllib raises nothing; you get an empty body and would happily write a
  0-byte file. Check the body, not the status alone.
- **The redirect filename carries the version** — parse it there rather than trusting the archive.
- **mirrors 404 on a delisted ID, they do not 500.** chrome-stats answers 403 (Cloudflare), extpose's
  viewer proxies the same dead Google URL, so it looks alive and returns nothing.
- **Watch the disk**: 1.5 GiB of CRX and 2.3 GiB unpacked. Unpacking is optional; if Isaac only wants
  packed files, delete `unpacked/` afterwards (he asked for that here).
- **A `__MSG_` name in an unpacked manifest is not a broken unzip** — it means the extension uses
  i18n and the display name lives in `_locales/*/messages.json`.
- `/analysis/chrome-crx-archive` is registered in `~/scripts/theme-inject.py` `SKIP_PREFIX`: the report
  is a print-precise artifact and the skin/beacon injection would disturb the measured layout.

## Deliverable

```
portfolio-deploy/
  analysis/chrome-crx-archive/
    report.html           8 sections, 7 tables, 131-row catalogue, self-contained
    crx-catalogue.csv     131 rows — category, name, ID, version, size, files, sha256, status
    crx-manifest.json     129 SHA-256 entries + the 2 delisted IDs marked status:delisted
  prompts/chrome-crx-archive-prompt.md   (this file)
```

Card actions: **Open the report** · **Catalogue (CSV)** · **Hash manifest (JSON)** ·
**Companion: policy audit**.
