Skip to content

feat(p1.9): mobile secure token storage via iOS Keychain#88

Merged
attson merged 8 commits into
mainfrom
feat/mobile-secure-storage
May 31, 2026
Merged

feat(p1.9): mobile secure token storage via iOS Keychain#88
attson merged 8 commits into
mainfrom
feat/mobile-secure-storage

Conversation

@attson
Copy link
Copy Markdown
Owner

@attson attson commented May 31, 2026

Summary

Implements P1.9 from the roadmap: relocate the mobile app's atterm.relay blob (containing the atk_… API token) from localStorage to iOS Keychain via a small custom Capacitor plugin. Migrate existing localStorage values on first read after upgrade, then clear them. Strengthen the "Allow insecure HTTP" warning UX.

  • Custom Capacitor plugin AttermSecureStorage (~80 lines Swift + ~10 lines Obj-C bridge). Single keychain item, kSecAttrService = "com.attson.atterm", kSecAttrAccessible = kSecAttrAccessibleAfterFirstUnlock. No third-party npm dep, no biometric prompt.
  • TS shim secureStorage.ts wraps the native plugin; falls back to in-memory Map on web + vitest via Capacitor's registerPlugin proxy. One-shot detection cached.
  • capacitor.ts migration on read: relay.load() checks Keychain first; if empty and localStorage has a legacy blob, writes it into Keychain then removes from localStorage. save() writes only to Keychain. clear() wipes both (defensive against interrupted migrations). fetchMe / listRemoteSessions consult both stores (Keychain first) so transitions are seamless.
  • InsecureBanner.vue: persistent top banner shown when the active relay URL is HTTP; tap to expand body, "Dismiss" to hide for the session. Returns next launch.
  • MobileSetup.vue: always-visible yellow warning block under the "Allow insecure" checkbox so the consequence is visible before opting in.
  • Info.plist unchangedNSAllowsArbitraryLoads = true stays (deliberate; per user decision). The deterrent moved to UI rather than runtime ATS.

Spec: `docs/superpowers/specs/2026-06-01-mobile-secure-storage-design.md`
Plan: `docs/superpowers/plans/2026-06-01-mobile-secure-storage.md`

Test Plan

  • `npm test` — 75 files / 628 tests green (6 new for secureStorage, 5 new for capacitor migration, 3 new for InsecureBanner)
  • `npx vue-tsc --noEmit` — clean
  • `npm run build:wails` — clean
  • `npm run build:capacitor` — clean
  • `go test ./...` — clean (regression gate for relay code unchanged)
  • manual: `cap sync ios` then Xcode build on simulator. Pair / enter manually, kill app, relaunch — credentials restored from Keychain, localStorage stays empty. Toggle off and on — InsecureBanner appears/disappears at the right times.

Out of scope (in spec §1)

  • Android Keystore (no Android project on disk yet)
  • Tightening NSAppTransportSecurity (user explicitly chose to keep permissive, strengthen UI instead)
  • Biometric prompt on read (`AfterFirstUnlock` is the right tier for background reconnect)

@attson attson merged commit 31b86ce into main May 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant