feat(release): production signing in build.yml + F-Droid metadata filled
- build.yml: use production keystore (KEYSTORE_BASE64) on tag pushes, fall back to debug key for PRs/branch builds — build.gradle already reads RELEASE_STORE_FILE env var so no Gradle changes needed - distribution/fdroid-submission/metadata.yml: filled AllowedAPKSigningKeys with actual SHA-256 fingerprint, commit tag updated to v0.3.1, version bumped to 0.3.1 - app.json: bump version 0.2.3 → 0.3.1, versionCode 1 → 2 - Add eas.json + EAS README for iOS App Store builds - Add fastlane/metadata/android for Play Store / F-Droid graphics - Add distribution docs: applestore, fdroid, market, playstore, security, threat-model, opencode-site-deploy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
151
docs/threat-model.md
Normal file
151
docs/threat-model.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Threat model — opencode-mobile
|
||||
|
||||
Date: 2026-05-24
|
||||
Version: 0.2.3
|
||||
|
||||
---
|
||||
|
||||
## Assets we protect
|
||||
|
||||
| Asset | Sensitivity | Storage |
|
||||
|---|---|---|
|
||||
| opencode server URL | Medium — reveals network topology | `expo-secure-store` (`opencode_connections`) |
|
||||
| HTTP Basic auth password for opencode server | High — grants full server access | `expo-secure-store` (`opencode_password_<id>`) |
|
||||
| User prompts sent to the AI model | High — may contain proprietary code or PII | In-flight only (not persisted on device) |
|
||||
| AI responses / generated code | High — proprietary output | In-flight only (not persisted on device) |
|
||||
| Sentry DSN | Low — public by design; quota-abuse risk | Bundle (EXPO_PUBLIC_) + SecureStore (consent state) |
|
||||
| Biometric/PIN auth settings | Low | `expo-secure-store` (`opencode_auth_settings`) |
|
||||
| Telemetry consent decision | Low | `expo-secure-store` (`opencode_telemetry_consent`) |
|
||||
| Session IDs | Low — server-assigned, ephemeral | In-memory only |
|
||||
| Notification preferences | Low | `expo-secure-store` (`opencode_settings`) |
|
||||
|
||||
---
|
||||
|
||||
## Attackers and attack scenarios
|
||||
|
||||
### 1. Network observer (passive MitM)
|
||||
|
||||
**Capability:** Can observe network traffic between phone and opencode server.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Intercept HTTP traffic on local LAN (e.g., corporate Wi-Fi, coffee shop).
|
||||
- Read user prompts and AI responses in cleartext if server is accessed via `http://`.
|
||||
|
||||
**Defenses:**
|
||||
- HTTPS: When user provides an `https://` URL, standard TLS verification via `expo/fetch` applies. No `rejectUnauthorized` overrides.
|
||||
- Documentation: App UI explicitly recommends using `https://` when TLS is configured; the quick-connect form defaults to `http://` only for LAN/Tailscale where the user has a trusted network path.
|
||||
- Tailscale: Most users access local servers over Tailscale (encrypted mesh VPN), making cleartext HTTP safe in practice.
|
||||
|
||||
**Residual risk:** Users who expose their opencode server on a public IP via HTTP (not HTTPS) without Tailscale have no transport-layer protection. This is documented as user responsibility.
|
||||
|
||||
---
|
||||
|
||||
### 2. Malicious deep link (`opencode://`)
|
||||
|
||||
**Capability:** Any installed app or web page can fire `opencode://` deep links at the app.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Inject a malicious server URL via deep link to phish the user into connecting to an attacker-controlled opencode server.
|
||||
- Cause the app to navigate to an attacker-controlled session ID.
|
||||
|
||||
**Defenses:**
|
||||
- Current deep link handling uses `expo-router`. The only parameterized routes are `/session/[id]` and `/connection/[id]`. Neither auto-connects to an external URL via deep link; server URLs are only stored via the explicit "Add Connection" UI.
|
||||
- No deep link handler was found that auto-creates a connection from URL parameters.
|
||||
- Notifications navigation (`router.push('/session/${data.sessionId}')`) uses server-assigned session IDs only, not user-supplied URLs.
|
||||
|
||||
**Residual risk:** Low. No auto-connect-from-deep-link path exists. If this is added in future, validate scheme+host against stored connections before navigating.
|
||||
|
||||
---
|
||||
|
||||
### 3. Lost or stolen device
|
||||
|
||||
**Capability:** Physical access to a locked device.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Read server URL and password from device storage.
|
||||
- View AI session content / conversation history from lock screen notifications.
|
||||
- Bypass app authentication.
|
||||
|
||||
**Defenses:**
|
||||
- All sensitive data in `expo-secure-store` (iOS Keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`; Android Keystore-backed EncryptedSharedPreferences). Data is inaccessible when the device is locked.
|
||||
- Optional biometric app lock (`requireBiometric` setting). Default off for UX; users with sensitive servers should enable it.
|
||||
- Notification lock-screen preview: session titles may appear in "Task completed" notifications. No prompt content or AI response content is included in notification bodies.
|
||||
- **Known gap (M-03):** `permission.asked` notifications can show tool file-path glob patterns on lock screen. Fix pending.
|
||||
|
||||
**Residual risk:** Medium. Without biometric lock enabled, a device that auto-unlocks (e.g., trusted location) exposes app contents. Users should be prompted to enable biometric lock during onboarding when a server is added.
|
||||
|
||||
---
|
||||
|
||||
### 4. Compromised opencode server
|
||||
|
||||
**Capability:** Attacker controls the opencode server the app connects to (e.g., via supply-chain, server compromise, or DNS hijacking to an HTTPS server).
|
||||
|
||||
**Attack scenarios:**
|
||||
- Server returns malicious tool permission requests designed to trick user into approving harmful file operations.
|
||||
- Server returns crafted SSE events to cause client-side errors and exfiltrate stack traces.
|
||||
|
||||
**Defenses:**
|
||||
- App renders server data (session titles, tool permission descriptions) as text only — no `eval()`, no `dangerouslySetInnerHTML`, no JavaScript injection surface.
|
||||
- Sentry reports are scrubbed of URLs and query params before upload.
|
||||
- Tool permission requests require explicit in-app user approval. The `permission.asked` UI shows the user what patterns the tool wants to access.
|
||||
|
||||
**Residual risk:** Medium. The app trusts the connected server by design. Users are responsible for connecting only to servers they control. This is documented in the UX.
|
||||
|
||||
---
|
||||
|
||||
### 5. Sentry DSN abuse
|
||||
|
||||
**Capability:** Attacker extracts the DSN from the APK/IPA (trivially readable via `strings`).
|
||||
|
||||
**Attack scenarios:**
|
||||
- Flood the Sentry project with fake events, exhausting quota and masking real errors.
|
||||
- Enumerate org/project metadata via the public DSN.
|
||||
|
||||
**Defenses:**
|
||||
- The Sentry DSN is `EXPO_PUBLIC_` and therefore intentionally public (this is Sentry's documented model).
|
||||
- `sendDefaultPii: false` ensures device identifiers are not sent.
|
||||
- Sentry ingest rate-limiting can be configured per-project (deferred enhancement).
|
||||
|
||||
**Residual risk:** Low. Quota exhaustion is a nuisance, not a security breach. No user data flows from DSN exposure.
|
||||
|
||||
---
|
||||
|
||||
### 6. CI/CD supply chain attack
|
||||
|
||||
**Capability:** Attacker compromises a third-party GitHub Action used in the build pipeline.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Malicious action exfiltrates `PLAY_STORE_SERVICE_ACCOUNT_JSON` or keystore secrets.
|
||||
- Tampered action injects malicious code into the APK.
|
||||
|
||||
**Defenses:**
|
||||
- Secrets are stored as GitHub Secrets, not in code.
|
||||
- `pull_request` trigger (not `pull_request_target`) prevents fork PRs from accessing secrets.
|
||||
- `KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD` are separate from `KEYSTORE_BASE64`, minimizing damage from any single secret leak.
|
||||
|
||||
**Residual risk:** Medium. Third-party actions are pinned to tags, not commit SHAs (see H audit finding M-02). SHA pinning is the recommended fix.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Security of the user's opencode server itself.** The server is user-managed; its authentication, file access controls, and model provider secrets are outside this app's threat model.
|
||||
- **Model provider security.** API keys for Anthropic, OpenAI, etc. are held by the opencode server, not the mobile app. The app never sees model provider credentials.
|
||||
- **Tailscale or VPN security.** Users connecting via Tailscale rely on Tailscale's security model for transport protection.
|
||||
- **Device operating system integrity.** Rooted/jailbroken devices can bypass `expo-secure-store`. This is a platform-level threat outside app scope.
|
||||
|
||||
---
|
||||
|
||||
## Security architecture summary
|
||||
|
||||
```
|
||||
[Phone] ──(HTTPS or HTTP-via-Tailscale)──> [opencode server (user-owned)]
|
||||
│ │
|
||||
│ expo-secure-store (Keychain/Keystore) │── AI model provider (user's keys)
|
||||
│ • server URL + auth password │
|
||||
│ • biometric settings │
|
||||
│ • telemetry consent │
|
||||
│ │
|
||||
│ opt-in only │
|
||||
└──(HTTPS)──> [Sentry ingest] (scrubbed, no PII, no prompts)
|
||||
```
|
||||
Reference in New Issue
Block a user