The bearer token was sha256(serial)[:32] and the serial is served unauthenticated, so anyone reaching :5000 could compute it and take the device over. Now: token is a random secrets.token_urlsafe(32) at /data/adacam/api_token (never derived from serial); /pair only returns it during a one-shot pairing window (/data/adacam/pairing_open, opened by adacam-pair or install.sh, closes after one pair); require_auth uses hmac.compare_digest. NEEDS ON-DEVICE PAIRING TEST before merge to main — see SECURITY-PAIRING.md.
2.7 KiB
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
- Random token, not derived.
auth.get_api_token()reads a high-entropy random token from/data/adacam/api_token(mode 600), lazily generating one withsecrets.token_urlsafe(32)on first use. It never depends on the serial. - One-shot pairing window.
/paironly returns the token when a window is open (/data/adacam/pairing_openexists). Opening the window requires physical/SSH presence on the device (adacam-pair, orinstall.shon first provision). The window closes the instant a pair succeeds — so a passive network attacker who hits/pairoutside a pairing event gets403. - Constant-time compare.
require_authuseshmac.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 <token>
Re-pair (new phone, reset): run adacam-pair again on the device.
On-device test checklist (do before merging to main)
- Fresh boot:
GET /pairwith no window →403. adacam-pair→GET /pair→ returns{serial, token}; secondGET /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_tokenis 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.