Files
keevault/docs/superpowers/specs/2026-05-21-mypass-design.md
T
2026-05-21 22:43:35 +02:00

9.7 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

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.

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

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.