# adacam-api auth + pairing model Rewritten 2026-06-13 (estate-audit CRIT fix). **Branch `feat/secure-pairing` — NOT yet deployed; needs an on-device pairing test before merge to `main`.** ## What was broken (the CRIT) The bearer token that authorizes device control (`/wifi/connect`, `/ssh/toggle`, `/wigle/config`, landmark ingest) was **derived from the device serial**: ``` token = sha256("adacam-api-{serial}-token")[:32] ``` …and the serial was handed out **unauthenticated** on `/pair`, `/api/1/info`, `/api/1/deviceinfo`. So anyone who could reach the device's `:5000` (its AP / LAN) could read the serial, compute the token, and take the device over. A serial is an identifier, not a secret — deriving auth from it is equivalent to no auth. ## The new model 1. **Random token, not derived.** `auth.get_api_token()` reads a high-entropy random token from `/data/adacam/api_token` (mode 600), lazily generating one with `secrets.token_urlsafe(32)` on first use. It never depends on the serial. 2. **One-shot pairing window.** `/pair` only returns the token when a window is open (`/data/adacam/pairing_open` exists). Opening the window requires physical/SSH presence on the device (`adacam-pair`, or `install.sh` on first provision). The window **closes the instant a pair succeeds** — so a passive network attacker who hits `/pair` outside a pairing event gets `403`. 3. **Constant-time compare.** `require_auth` uses `hmac.compare_digest`. The serial is still returned by `/api/1/info` (the app uses it as a non-secret device id and reachability probe). That is now harmless — it no longer yields the token. The "serial disclosure" item is an accepted low residual, not a CRIT. ## Pairing flow ``` device: adacam-pair # opens the one-shot window app: GET /pair -> { serial, token, ... } # window closes here app: stores token; all control calls send Authorization: Bearer ``` Re-pair (new phone, reset): run `adacam-pair` again on the device. ## On-device test checklist (do before merging to main) - [ ] Fresh boot: `GET /pair` with no window → `403`. - [ ] `adacam-pair` → `GET /pair` → returns `{serial, token}`; second `GET /pair` → `403`. - [ ] Control endpoint with the returned token → `200`; with a wrong/empty token → `401`. - [ ] Old serial-derived token (`sha256("adacam-api-$serial-token")[:32]`) → `401`. - [ ] `/data/adacam/api_token` is mode 600, survives a service restart (token stable). - [ ] Varroa app pairs, stores the token, and drives wifi/ssh/wigle successfully. ## install.sh `install.sh` opens the window once on a fresh provision (`touch /data/adacam/pairing_open`) so the first pair works out of the box; it closes after that first pair.