- 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
4.6 KiB
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
- Generate a token.
hermes gateway setup(ourinteractive_setup) either uses an existingANDROID_TOKENor generates a fresh high-entropy token (e.g. 32 bytes → 64 hex chars) and stores it in.env. - 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.
- QR code: the setup prints a QR encoding
- App connects. First WS frame is
hello {token, device_id, device_name, caps, fcm_token?}. - Server verifies. Constant-time compare of
tokenvsANDROID_TOKEN(hmac.compare_digest). Optionally checkdevice_idagainstANDROID_ALLOWED_USERS(if set) orANDROID_ALLOW_ALL_USERS. - On success: register the device in
devices.db, sendhello.ack. On failure: senderror {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_TOKENis authorized (it's the user's own token). - Allowlist (optional):
ANDROID_ALLOWED_USERS(comma-separateddevice_ids) restricts which devices may connect even with the token — useful if the token is shared.ANDROID_ALLOW_ALL_USERS=truedisables 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_TOKENinvalidates 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.pypatterns). device_idis a random UUID (not PII).device_nameis 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_secretpattern (seeplugins/platforms/irc/adapter.py:42) so multiplexed profiles don't leak each other's tokens (fail-closed undergateway.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.