diff --git a/docs/ai/adr/013-smartphone-proximity-token.md b/docs/ai/adr/013-smartphone-proximity-token.md new file mode 100644 index 0000000..8d22c04 --- /dev/null +++ b/docs/ai/adr/013-smartphone-proximity-token.md @@ -0,0 +1,199 @@ +# ADR-013: Smartphone as Proximity Token + +## Status +Accepted + +## Context +SunRay's session mobility was driven by smart card insertion/removal. WayRay supports pluggable tokens (ADR-004). A smartphone is the one device users always carry. If the phone can act as a proximity token, we get automatic session follow without any explicit action -- walk up to a terminal, your desktop appears; walk away, it suspends. + +This maps directly to SunRay's smart card semantics: +- Smart card insert → phone enters proximity range +- Smart card remove → phone leaves proximity range + +But better: no physical insertion, works from your pocket. + +## Technology Options + +### BLE (Bluetooth Low Energy) -- Recommended + +The phone runs a small companion app that broadcasts a BLE advertisement containing a session token. The WayRay client has a BLE receiver that detects nearby phones. + +**How BLE beacons work:** +- Phone advertises a BLE beacon with a service UUID specific to WayRay +- The advertisement payload contains an encrypted session token (≤31 bytes in legacy advertising, ≤255 bytes in extended) +- The client scans for WayRay beacons and reads the token +- RSSI (signal strength) determines proximity -- configurable threshold +- When RSSI drops below threshold (user walked away), trigger disconnect + +**Advantages:** +- Always-on: phone advertises in background, no user action needed +- Works from pocket (no need to pull phone out) +- Range tunable via RSSI threshold (1-10 meters typical) +- Low power: BLE advertising uses ~1-5% battery per day +- Universal: every modern smartphone has BLE +- Works through walls at close range (meeting rooms) + +**BLE Token Flow:** +``` +Phone (companion app): + 1. User authenticates in app once (OIDC, biometric, etc.) + 2. App receives signed session token from IdP/server + 3. App begins BLE advertising: + Service UUID: WayRay-specific + Payload: encrypted(token_id + timestamp + HMAC) + 4. Rotates payload periodically (replay prevention) + +WayRay Client (BLE scanner): + 1. Continuously scans for WayRay service UUID + 2. Detects beacon → reads token → validates HMAC + timestamp + 3. If new token in range: trigger session attach + 4. If token leaves range (RSSI below threshold for N seconds): + trigger session detach + 5. If multiple tokens: nearest (highest RSSI) wins +``` + +### NFC -- Complementary + +Phone tap on NFC reader for explicit authentication: +- Quick deliberate action (tap to connect) +- Works as fallback when BLE is disabled +- Can trigger initial token provisioning +- Very short range (~4cm) -- no proximity tracking + +### UWB (Ultra-Wideband) -- Future + +Precise distance measurement (10cm accuracy): +- iPhone U1/U2 chip, some Samsung/Google phones +- Could enable "desk assignment" -- know exactly which terminal you're closest to +- Not yet universal enough to depend on +- Consider as enhancement when hardware penetration increases + +### WiFi Proximity (mDNS) -- Fallback + +Phone app announces presence on local network: +- Works without BLE hardware on client +- Coarse proximity (same VLAN/subnet) +- Higher latency (mDNS discovery takes seconds) +- Can't distinguish between terminals in the same room +- Useful as a fallback when BLE isn't available + +## Decision + +**BLE as primary proximity mechanism, NFC as explicit-action complement, WiFi/mDNS as software-only fallback.** + +### Proximity State Machine + +``` + BLE beacon detected + (RSSI > threshold) + [No Phone] ─────────────────────────────────> [Detected] + │ + │ stable for + │ T_attach seconds + v + ┌──────────────────────────── [Attached] + │ │ + │ BLE beacon returns │ RSSI < threshold + │ (RSSI > threshold) │ for T_detach seconds + │ v + └──────────────────────────── [Detaching] + │ + │ timeout expires + v + [Detached] + │ + │ (session suspends) + v + [No Phone] +``` + +**Timers prevent flapping:** +- `T_attach`: delay before attaching (default: 2 seconds). Prevents drive-by session grabs when walking past a terminal. +- `T_detach`: delay before detaching (default: 10 seconds). Prevents session drop when phone briefly loses signal (body shielding, phone rotates in pocket). +- Both configurable per-deployment. + +### Security Considerations + +**Replay attacks:** Token payload includes a timestamp and HMAC. Client rejects tokens older than N seconds. Phone rotates payload every 30 seconds. + +**Relay attacks:** An attacker could relay the BLE signal from a distant phone to a nearby client. Mitigations: +- Token payload includes a challenge-response nonce (requires phone app to respond) +- RSSI-based distance bounding (relayed signals have abnormal RSSI patterns) +- Optional: require NFC tap for initial session attachment, BLE only for persistence +- For high-security deployments: disable BLE proximity, use smart card only + +**Token theft:** If someone clones the BLE advertisement, they get the session token. Mitigations: +- Token rotation (new token every 30s, phone signs each one) +- Mutual authentication: client challenges phone via BLE GATT connection +- Binding token to phone's hardware attestation key (Android SafetyNet / iOS DeviceCheck) + +**Multi-phone scenarios:** When multiple phones are near a terminal: +- Highest RSSI wins (closest phone) +- If tie: first-arrived wins +- Explicit NFC tap overrides BLE proximity (deliberate action beats passive detection) + +### Hardware Requirements + +**Client side:** +- BLE 4.0+ receiver (USB dongle or built-in) +- Optional: NFC reader (USB) +- Commodity hardware: USB BLE dongles cost ~$5-10 + +**Phone side:** +- Companion app (Android + iOS) +- BLE 4.0+ (every phone since ~2013) +- Background execution permission for BLE advertising + +### Implementation as Auth Plugin + +```rust +struct BleProximityPlugin { + scanner: BleScanner, + known_tokens: HashMap, + rssi_threshold: i8, // e.g., -70 dBm + attach_delay: Duration, // e.g., 2 seconds + detach_delay: Duration, // e.g., 10 seconds +} + +impl TokenProvider for BleProximityPlugin { + fn watch(&self) -> TokenEventStream { + // Emits: + // TokenInserted(token_id) -- phone entered proximity + // TokenRemoved(token_id) -- phone left proximity + } +} +``` + +This implements the same `TokenProvider` trait as smart cards (ADR-004). The session management layer doesn't know or care whether the token came from a smart card slot or a BLE beacon. + +### Companion App Scope + +The phone app is intentionally minimal: +1. One-time setup: authenticate with IdP (OIDC), receive signing key +2. Background service: broadcast BLE beacon with rotating signed token +3. Optional: respond to GATT challenges for mutual auth +4. Optional: show notification when session attaches/detaches +5. No remote desktop functionality -- the phone is a **key**, not a viewer + +Platform: +- Android: foreground service with BLE advertising +- iOS: Core Bluetooth peripheral mode (works in background with limitations) +- Could be a PWA using Web Bluetooth (limited background support) + +## Rationale + +- **Zero-friction session mobility**: walk up, session appears. Walk away, session suspends. No card to insert, no button to press, no QR to scan. +- **Users already carry phones**: unlike smart cards which are an additional device to manage and can be forgotten +- **Maps to SunRay semantics**: insert/remove maps to enter/leave proximity. Same session management, different physical mechanism. +- **Pluggable**: implements `TokenProvider` trait. Composable with other token types. Smart card overrides BLE on explicit insertion. +- **Tunable security/convenience tradeoff**: high security deployments add NFC tap requirement or disable BLE entirely. Casual deployments use pure proximity. + +## Consequences + +- Requires BLE hardware on client devices (USB dongle if not built-in) +- Must develop and maintain companion apps for Android and iOS +- BLE scanning has power implications on battery-powered clients +- RSSI is noisy and affected by environment (walls, bodies, interference). Threshold tuning is deployment-specific. +- iOS background BLE advertising has limitations (Apple throttles frequency) +- Must handle edge cases: phone in adjacent room, phone dies mid-session, multiple phones +- Privacy consideration: BLE beacons are detectable by nearby devices. Token payload must be encrypted so only WayRay clients can read the session token.