Security isn't a feature in Termopus — it's the architecture. Every design decision starts with one question: what's the strongest identity available? The phone's Secure Enclave / StrongBox holds hardware-bound keys that never leave the chip. The bridge runs on disk-based files that can be copied. All enforcement lives on the phone side.
Layer 7 ┃ Biometric Gate Face ID / Fingerprint on every session
┃
Layer 6 ┃ Device Attestation App Attest (iOS) + Play Integrity (Android)
┃
Layer 5 ┃ Cloudflare Zero Trust mTLS + device posture enforcement
┃
Layer 4 ┃ Cloudflare Tunnel No open ports — outbound connections only
┃
Layer 3 ┃ E2E Encryption AES-256-GCM — relay forwards opaque blobs
┃
Layer 2 ┃ Hardware-Backed Keys Secure Enclave (iOS) / StrongBox (Android)
┃
Layer 1 ┃ Certificate Pinning Relay identity verified on every connection
Every session requires biometric authentication before any action is allowed. The lock screen activates on:
- Cold start (app launch)
- Resume after configurable timeout (default: 5 minutes)
- Session pairing
Biometric proof is cryptographic — BiometricCryptoService generates a signed proof via native crypto, not just a boolean true/false. The proof is one-time-use; replaying it triggers a security trap.
Before a device can receive a certificate, it must prove it's genuine hardware running an unmodified app:
| Platform | Mechanism | Verification |
|---|---|---|
| iOS | App Attest | Apple's attestation service validates device + app bundle |
| Android | Play Integrity | Google's integrity API validates device + app signing |
Attestation happens during provisioning (certificate issuance). Jailbroken devices and emulators are rejected.
The relay lives behind Cloudflare Access with mTLS enforcement:
- Only devices with provisioned client certificates can connect
- Device posture checks validate certificate fingerprint against KV store
CF-Access-Client-Idheader carries the verified device identity
The bridge never opens inbound ports. All connections are outbound WebSocket to Cloudflare's edge:
- No port forwarding, no firewall rules, no VPN
- Bridge initiates connection — nothing can connect to it
- QR code contains the relay room ID + public key — that's all
Every message is encrypted before it leaves the device:
Phone Relay Bridge
│ │ │
│ AES-256-GCM(msg) │ │
├───────────────────────►│ opaque blob │
│ ├───────────────────────►│
│ │ │ decrypt(blob) → msg
│ │ │
│ │ AES-256-GCM(reply) │
│ opaque blob │◄───────────────────────┤
│◄───────────────────────┤ │
│ decrypt(blob) → reply │ │
- Algorithm: AES-256-GCM with per-session keys
- Key Exchange: X25519 ECDH between phone hardware key and bridge
- Key Derivation: HKDF-SHA256
- The relay is mathematically unable to read your data
Encryption keys are generated inside tamper-resistant hardware and never leave:
| Platform | Hardware | Key Type | Extractable? |
|---|---|---|---|
| iOS | Secure Enclave | EC P-256 | No — hardware-bound |
| Android | StrongBox / TEE | EC P-256 | No — hardware-bound |
Even if an attacker has full filesystem access, they cannot extract the session keys.
The app verifies the relay's identity on every connection using pinned certificates. MITM attacks against the relay connection are detected and rejected. Certificate rotation is supported via a remote toggle (safety valve for CA changes).
The bridge implements a 3-tier permission system for every Claude Code tool call:
Incoming tool request
│
▼
┌───────────────┐ Block all writes
│ Mode Override │────► (e.g., "plan" mode)
└───────┬───────┘
│ pass
▼
┌───────────────┐ Match glob patterns
│ Rule Matching │────► "Bash(git *)" → Allow
└───────┬───────┘ "Edit(/etc/*)" → Deny
│ no match
▼
┌───────────────┐ Interactive card
│ Phone Fallback│────► on user's phone
└───────────────┘
- "Always Allow" auto-writes glob rules to
settings.local.json - Rules persist across sessions — the bridge learns your preferences
AskUserQuestionintercepted and rendered as interactive cards
| Threat | Mitigation |
|---|---|
| Trial abuse | IP rate limit (5/hour) + deviceId uniqueness |
| Webhook forgery | HMAC-SHA256 verification (Paddle H1 signatures) |
| Brute-force restore | 3 attempts + 15-minute lockout, constant-time comparison |
| Corrupt subscription | Fail-closed — 500 error, deny access |
| Email enumeration | Identical responses for found/not-found |
Every catch block on a security-critical path denies access. There are no silent fallbacks, no "allow on error," no "try again later." If something is wrong, the answer is no.
