Files
iris_x_hermes/docs/09-pairing-security.md
T
ARIA 59acf66c89 M0: toolchain, monorepo scaffold, gateway plugin skeleton, CMP app
- gateway-plugin/: android platform plugin (plugin.yaml + adapter.py
  register(ctx) + no-op AndroidAdapter) + stub modules for M1-M5
- app/: Compose Multiplatform project (shared KMP + androidApp +
  desktopApp) with Gradle wrapper; builds :androidApp:assembleDebug
  and :desktopApp:compileKotlin
- scripts/guard_hermes_agent.sh + pre-commit hook: fail if hermes-agent/
  is staged (read-only reference, never committed)
- .gitignore excludes hermes-agent/; docs/ reference library
2026-08-19 11:27:02 +02:00

4.6 KiB

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=<lan-ip>&port=8790&token=<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_ids) 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.