Files
keevault/docs/superpowers/specs/2026-05-21-mypass-design.md
T
krissandClaude Sonnet 5 837786f4d9 docs: resolve remaining open questions in design spec
Research confirmed macOS AutoFill has no real scope reduction vs iOS
(same API, same app-or-website coverage) beyond requiring manual
per-platform enablement, and that Associated Domains is the
responsibility of the app being logged into, not the password
manager's extension -- MyPass's existing CredentialMatcher already
handles both domain- and bundle-ID-based serviceIdentifiers correctly.
Adds an onboarding note since enabling the extension can't be
automated on either platform.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 15:47:22 +02:00

11 KiB

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

AutoFill (App Extension Target)  ──links──► MyPassCore
├── ASCredentialProviderViewController subclass
├── CredentialListView    — filtered by serviceIdentifier URL/domain
└── UnlockView (mini)     — Face ID → password fallback

Shared App Group (group.org.antiloop222.mypass)
├── UserDefaults          — security-scoped bookmark, last-opened vault, settings
└── Keychain Access Group — master password, biometric token (org.antiloop222.mypass)

External dependencies (SPM)

  • KDBX parsing: KeePassKit (Objective-C, MIT). No upstream SPM support, so vendored locally as a git submodule (Vendor/KeePassKit) with a hand-written Package.swift. This pulled in the same problem three more times — none of the following had clean drop-in SPM support either, so all are vendored the same way:
    • KissXML (Vendor/KissXML) — XML parsing KeePassKit depends on
    • Argon2 (nested git submodule inside Vendor/KeePassKit) — KDBX4 key derivation
    • ChaCha20 / TwoFish — cipher implementations already bundled in KeePassKit's own source tree, wired into the SPM target
  • 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.

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 UnlockViewLAContext 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

Onboarding

Neither iOS nor macOS allows the extension to be pre-enabled — the user must turn it on manually (Settings → Passwords → AutoFill Passwords on iOS; System Settings → Extensions on macOS). The main app's first-launch flow should detect this and prompt the user with a deep link to the relevant settings screen.

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

  1. KDBX library: Evaluate Swift Package Manager options that support KDBX 3.1 + 4.0 with Argon2. Fallback: KeePassKit (Objective-C via bridging header). Resolved: no SPM-native option was found; KeePassKit is used, vendored locally (see External dependencies above).
  2. macOS AutoFill: ASCredentialProviderExtension on macOS 13+ has reduced scope vs iOS — verify which apps support third-party fill on macOS and document limitations. Resolved: no scope reduction found. Same ASCredentialProviderViewController API and "app or website" coverage as iOS. The only difference is enablement — the user must turn the extension on manually (System Settings → Extensions on macOS, Settings → Passwords → AutoFill on iOS); neither platform allows pre-enabling it via MDM/config profile. Onboarding should prompt the user to do this on first launch, on both platforms.
  3. Associated Domains: For bundle-ID-based AutoFill matching, an associated domains file may be needed for first-party apps. Evaluate at implementation time. Resolved: not applicable. Associated Domains is the responsibility of the app being logged into (so the system can pass a domain-based serviceIdentifier instead of a bundle-ID one) — not the password manager. MyPass already handles both cases correctly: CredentialMatcher just matches whatever serviceIdentifier the system supplies against stored entry URLs, the same approach 1Password/Bitwarden use. No work needed.