- 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>
7.8 KiB
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 viaexpo/fetchapplies. NorejectUnauthorizedoverrides. - Documentation: App UI explicitly recommends using
https://when TLS is configured; the quick-connect form defaults tohttp://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 withkSecAttrAccessibleWhenUnlockedThisDeviceOnly; Android Keystore-backed EncryptedSharedPreferences). Data is inaccessible when the device is locked. - Optional biometric app lock (
requireBiometricsetting). 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.askednotifications 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(), nodangerouslySetInnerHTML, 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.askedUI 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: falseensures 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_JSONor keystore secrets. - Tampered action injects malicious code into the APK.
Defenses:
- Secrets are stored as GitHub Secrets, not in code.
pull_requesttrigger (notpull_request_target) prevents fork PRs from accessing secrets.KEYSTORE_PASSWORD,KEY_ALIAS,KEY_PASSWORDare separate fromKEYSTORE_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)