# 09 — Pairing, Auth & Security ## 9.1 Threat model - **Trusted domain:** a personal agent on the user's own machine; one owner, a few of their own devices (phone + desktop). - **Primary risks:** (a) an unauthorized device connecting to the WS and reading/driving the agent; (b) eavesdropping on the WS in transit; (c) token leakage in logs; (d) arbitrary file read via media pull. - **Not addressing (v1):** multi-tenant isolation, adversarial multi-user abuse, E2E encryption. ## 9.2 Pairing flow 1. **Generate a token.** `hermes gateway setup` (our `interactive_setup`) either uses an existing `ANDROID_TOKEN` or generates a fresh high-entropy token (e.g. 32 bytes → 64 hex chars) and stores it in `.env`. 2. **Present to the app.** Two options: - **QR code:** the setup prints a QR encoding `iris://pair?host=&port=8790&token=` (or a WSS URL). The phone scans it with the app's camera (or a system scanner) → pre-fills settings. - **Manual:** user types the server URL + token in the app's Connect screen. 3. **App connects.** First WS frame is `hello {token, device_id, device_name, caps, fcm_token?}`. 4. **Server verifies.** Constant-time compare of `token` vs `ANDROID_TOKEN` (`hmac.compare_digest`). Optionally check `device_id` against `ANDROID_ALLOWED_USERS` (if set) or `ANDROID_ALLOW_ALL_USERS`. 5. **On success:** register the device in `devices.db`, send `hello.ack`. **On failure:** send `error {code:"auth"}` and close. `device_id` is a stable, app-generated UUID (persisted in the app's secure storage). It identifies the device for routing + push, **not** as a security principal (the token is). ## 9.3 Auth model - **Token = the security principal.** Any connection presenting the valid `ANDROID_TOKEN` is authorized (it's the user's own token). - **Allowlist (optional):** `ANDROID_ALLOWED_USERS` (comma-separated `device_id`s) restricts which *devices* may connect even with the token — useful if the token is shared. `ANDROID_ALLOW_ALL_USERS=true` disables the allowlist (dev only). - **Per-device tokens (stretch):** mint a unique token per device at pairing (revocable) instead of one shared token. v1 uses the shared token + optional device allowlist. - **Re-pairing:** rotating `ANDROID_TOKEN` invalidates all devices; they must re-pair. `hermes android pair` (stretch CLI) re-issues + prints a new QR. ## 9.4 Transport security - **Default (LAN/dev):** plain `ws://` on the trusted LAN. Fine for a home network. - **WSS (recommended for remote):** set `ANDROID_WS_CERT` / `ANDROID_WS_KEY` (self-signed or CA-signed). The app pins/accepts the cert (self-signed → user confirms fingerprint on first pair, like a SSH host key). - **Remote reachability options** (documented, user's choice): - **Tailscale / WireGuard** (recommended): gateway gets a stable tailnet IP; app connects over the private mesh. No public exposure. - **Reverse proxy / tunnel** (Caddy, Cloudflare Tunnel, ngrok): terminate TLS at the edge, forward WS to `127.0.0.1:8790`. - **Public bind** (`0.0.0.0`) + WSS + strong token — last resort. - The app stores the server URL + (for self-signed) the pinned cert fingerprint in secure storage. ## 9.5 Secret & PII handling - **Tokens/keys never logged.** Redact `ANDROID_TOKEN`, FCM tokens/keys, ntfy tokens in all log output (hermes PII policy; `agent/redact.py` patterns). - **`device_id`** is a random UUID (not PII). `device_name` is user-chosen. - **Media pull** is gated by hermes `validate_media_delivery_path` + delivery root/recency/denied-path checks (`gateway/platforms/base.py:1684`) — the plugin can only serve files hermes is allowed to deliver (no arbitrary file read). - **Search** is local-only (user's own hermes home); no data leaves the machine. ## 9.6 Profile safety - All plugin state lives under `get_hermes_home()/"android"` (profile-aware). - Secrets are read with the scope-aware `_get_scoped_secret` pattern (see `plugins/platforms/irc/adapter.py:42`) so multiplexed profiles don't leak each other's tokens (fail-closed under `gateway.multiplex_profiles`). - The WS bind uses a scoped lock (`gateway.status.acquire_scoped_lock`) so two profiles can't bind the same port/identity. ## 9.7 Hardening checklist - [ ] Constant-time token compare. - [ ] Bounded per-connection send buffer + rate limit on inbound frames. - [ ] Reject oversized frames / uploads (`max_upload_bytes`). - [ ] Verify media sha256 + re-sniff MIME (don't trust client). - [ ] Redact all secrets in logs. - [ ] WSS + cert pinning for remote. - [ ] Outbox retention cap + prune. - [ ] Fail-closed secret reads under multiplexing.