diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a95436 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.superpowers/ diff --git a/docs/superpowers/specs/2026-05-21-mypass-design.md b/docs/superpowers/specs/2026-05-21-mypass-design.md new file mode 100644 index 0000000..746badb --- /dev/null +++ b/docs/superpowers/specs/2026-05-21-mypass-design.md @@ -0,0 +1,238 @@ +# MyPass — Design Spec + +**Date:** 2026-05-21 +**Status:** Approved + +--- + +## Overview + +MyPass is a iOS + macOS password manager that reads and writes KeePass KDBX vaults. Users pick an existing `.kdbx` file from anywhere in the Files system (iCloud Drive, Dropbox, local storage) and MyPass becomes their viewer/editor for that vault. A bundled AutoFill Credential Provider extension enables password fill-in across every app on the device — not just Safari. + +### Goals +- Open, browse, add, edit, and delete entries in an existing KDBX vault +- Unlock with master password once; Face ID / Touch ID on subsequent opens +- AutoFill credentials into any app via `ASCredentialProviderViewController` +- Support full KeePass entry structure: groups/subgroups, custom fields, TOTP, attachments + +### Non-goals +- Creating new KDBX vaults from scratch +- Syncing or hosting vaults (the user owns the file) +- Web browser extension + +--- + +## Architecture + +### Option chosen: Local Swift Package + App Extension + +A local Swift Package (`MyPassCore`) holds all vault logic. Both targets link to it. A shared App Group bridges the two processes. + +``` +MyPassCore (Swift Package) +├── KDBXDocument — parse and write KDBX 3.1 / 4.0 +├── Entry / Group / Attachment — value-type models +├── TOTPGenerator — RFC 6238 TOTP from otp:// custom field +├── VaultSession — owns the live KDBXDatabase; lock/unlock lifecycle +└── KeychainStore — read/write master password in shared Keychain group + +MyPass (Main App Target) ──links──► MyPassCore +├── UnlockView +├── GroupBrowserView +├── EntryDetailView +├── EntryEditView +├── SearchView +├── FileBookmarkService — security-scoped bookmark management +├── BiometricAuthService — LAContext wrapper +└── ClipboardService — copy with 30 s expiry + +AutoFillExtension (App Extension Target) ──links──► MyPassCore +├── ASCredentialProviderViewController subclass +├── CredentialListView — filtered by serviceIdentifier URL/domain +└── UnlockView (mini) — Face ID → password fallback + +Shared App Group (group.com.christophevila.mypass) +├── UserDefaults — security-scoped bookmark, last-opened vault, settings +└── Keychain Access Group — master password, biometric token +``` + +**External dependencies (SPM)** +- A KDBX parsing library (to be confirmed during implementation — evaluate available Swift/ObjC options supporting KDBX 3.1 and 4.0 with Argon2 KDF) +- Apple CryptoKit (built-in) +- LocalAuthentication (built-in) +- AuthenticationServices (built-in) + +--- + +## Data Model + +All types are value types (`struct`) defined in `MyPassCore`. `VaultSession` is the single owner of a live `KDBXDatabase`. + +```swift +struct KDBXDatabase { + var metadata: DatabaseMetadata // name, description, cipher, compression + var root: Group +} + +struct Group: Identifiable { + var id: UUID + var name: String + var iconIndex: Int + var subgroups: [Group] // recursive tree + var entries: [Entry] +} + +struct Entry: Identifiable { + var id: UUID + var title: String + var username: String + var password: ProtectedString + var url: String + var notes: String + var customFields: [CustomField] + var attachments: [Attachment] + var totp: TOTPConfig? // parsed from otp:// custom field + var tags: [String] + var iconIndex: Int + var expiryDate: Date? + var creationDate: Date + var modificationDate: Date + var history: [Entry] // read-only previous versions +} + +struct CustomField: Identifiable { + var id: UUID + var key: String + var value: ProtectedString +} + +struct ProtectedString { + private(set) var value: String // XOR-obfuscated in memory + var isProtected: Bool + func reveal() -> String // decode on demand only +} + +struct Attachment: Identifiable { + var id: UUID + var name: String + var data: Data +} + +struct TOTPConfig { + var secret: String + var period: Int // default 30s + var digits: Int // default 6 + var algorithm: TOTPAlgorithm // SHA1 / SHA256 / SHA512 +} +``` + +`ProtectedString` XOR-obfuscates its value in memory and only decodes on explicit `reveal()` calls. It never appears in logs or debug descriptions. + +--- + +## UI Navigation + +### iOS — NavigationStack + +``` +UnlockView + └─► GroupBrowserView (root group) + ├─► GroupBrowserView (subgroup, pushed recursively) + └─► EntryDetailView + └─► EntryEditView (sheet) +``` + +- Search bar at the top of every `GroupBrowserView` filters entries globally across all groups +- Swipe-to-delete on entries; toolbar `+` button adds an entry in the current group +- Tapping a password field in `EntryDetailView` copies to clipboard (clears after 30 s) +- TOTP code displays with a countdown ring and refreshes automatically + +### macOS — NavigationSplitView (3 columns) + +| Sidebar | Middle | Detail | +|---|---|---| +| Group tree (expandable) | Entry list for selected group + search bar | Entry fields + Edit button | + +Keyboard shortcuts: `⌘C` copies password, `⌘⌥C` copies TOTP code, `⌘E` opens edit sheet. + +--- + +## AutoFill Credential Provider Extension + +**Entitlement:** `com.apple.developer.authentication-services.autofill-credential-provider` +**Extension point:** `com.apple.authentication-services-credential-provider-ui` + +### Runtime flow + +1. User taps a login field in any app → iOS shows QuickType bar with MyPass icon +2. User taps the icon → iOS instantiates `ASCredentialProviderViewController` and calls `prepareCredentialList(for: [ASCredentialServiceIdentifier])` +3. Extension checks Keychain for a valid biometric token: + - **Token valid (vault was recently unlocked):** skip to step 5 + - **Token missing/expired:** show mini `UnlockView` → `LAContext` Face ID / Touch ID → password fallback +4. Extension resolves the security-scoped bookmark from shared App Group `UserDefaults`, opens the KDBX file, decrypts using the master password from the shared Keychain group +5. `CredentialListView` displays entries filtered by matching the `serviceIdentifier` URL/domain against each entry's URL field. Search bar allows manual lookup across all entries +6. User taps an entry → `completeRequest(withSelectedCredential: ASPasswordCredential(user:password:))` → field filled → extension dismissed + +### URL matching + +Entry URL field is matched against the `serviceIdentifier` using host comparison (e.g. `github.com` matches `https://github.com/login` and bundle ID-based identifiers via Associated Domains). Entries with no URL are shown in a separate "All entries" section below suggestions. + +### Extension constraints + +- Memory limit ~50 MB: KDBX is parsed fresh on each extension invocation; no SwiftData or persistent cache inside the extension +- If KDBX parse fails, show an error alert with an "Open MyPass" deep-link button (`mypass://unlock`) + +--- + +## Security Model + +| Concern | Implementation | +|---|---| +| Master password at rest | Keychain item: `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`, shared via Keychain Access Group | +| Biometric unlock | `LAContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics)` retrieves master password from Keychain on success | +| File access | Security-scoped bookmark in shared App Group `UserDefaults`; `startAccessingSecurityScopedResource()` / `stopAccessingSecurityScopedResource()` bracket every read/write | +| In-memory passwords | `ProtectedString` XOR-obfuscates values; decoded only on `reveal()` | +| Clipboard | `UIPasteboard` item set with 30-second expiration using `UIPasteboard.setItems(_:options:)` with `UIPasteboardOptionExpirationDate` | +| KDBX crypto | Fully delegated to the KDBX parsing library (AES-256-CBC / ChaCha20, Argon2d / AES-KDF) — no custom crypto | +| Vault lock on background | `VaultSession.lock()` called in `sceneDidEnterBackground` — zeroes master password from memory; biometric re-auth required on next foreground | + +--- + +## Error Handling + +| Scenario | Behaviour | +|---|---| +| Wrong master password | Alert: "Incorrect password" — no retry limit | +| Corrupt or unsupported KDBX | Alert with file name and format error message | +| Bookmark stale (file moved/deleted) | Alert: "Vault file not found" + "Choose file…" button | +| Biometrics unavailable | Graceful fallback to password entry field (both main app and extension) | +| Extension parse failure | Error screen + "Open MyPass" deep-link button | +| Save conflict (file modified externally) | Detect via `NSFileCoordinator`; alert user and offer to reload or overwrite | + +--- + +## Testing + +### `MyPassCoreTests` (unit) +- Parse fixture `.kdbx` files (KDBX 3.1 and 4.0 with both AES-KDF and Argon2d) +- Round-trip: parse → modify entry → write → re-parse → assert equality +- `TOTPGenerator`: known test vectors from RFC 6238 +- `ProtectedString`: obfuscation / reveal symmetry +- AutoFill URL matching: table-driven tests covering exact match, subdomain, no-URL entries + +### `MyPassTests` (integration) +- Unlock flow with a test KDBX fixture +- Add / edit / delete entry persisted to file + +### `MyPassUITests` +- Unlock → browse groups → view entry detail → copy password +- Add entry → verify it appears in list +- Search: query returns expected entries + +--- + +## Open Questions (resolved at implementation time) + +1. **KDBX library:** Evaluate Swift Package Manager options that support KDBX 3.1 + 4.0 with Argon2. Fallback: KeePassKit (Objective-C via bridging header). +2. **macOS AutoFill:** `ASCredentialProviderExtension` on macOS 13+ has reduced scope vs iOS — verify which apps support third-party fill on macOS and document limitations. +3. **Associated Domains:** For bundle-ID-based AutoFill matching, an associated domains file may be needed for first-party apps. Evaluate at implementation time.