The Passbox app for iPhone and iPad (iOS 17 or later) is a SwiftUI app with an AutoFill Credential Provider extension. All cryptography, sync and the offline cache come from the Rust client of passboxnet/core (crates/client) through its UniFFI bindings (crates/ffi), the same code the CLI uses.
Architecture
.
├── project.yml XcodeGen definition (the .xcodeproj is generated, not committed)
├── Config/Base.xcconfig Shared settings; includes an optional, uncommitted Signing.xcconfig
├── Passbox/ App target: SwiftUI screens, keyboard shortcuts, identity store updates
├── PassboxAutoFill/ AutoFill extension (ASCredentialProviderViewController)
├── Shared/ SwiftUI code and assets used by both targets (theme, unlock panel)
├── PassboxKit/ Swift package: everything that does not need UIKit/SwiftUI
│ ├── Sources/PassboxBindings Generated UniFFI Swift bindings (not committed)
│ ├── Sources/PassboxKit Service, storage, view models, matching, formatting
│ ├── Sources/passbox_ffiFFI Module map for Linux (the header is generated)
│ ├── Frameworks/ PassboxFFI.xcframework (built, not committed)
│ └── Tests/ Unit tests and integration tests against a real server
└── scripts/
├── generate-bindings.sh Generates the Swift bindings into PassboxKit
├── build-xcframework.sh Builds the Rust static libraries and PassboxFFI.xcframework (macOS)
└── test-kit.sh Builds what the tests need and runs `swift test` SwiftUI views (Passbox, PassboxAutoFill) @MainActor
│ observe
▼
VaultModel, AppSettings, GeneratorModel, TotpModel @MainActor @Observable (PassboxKit)
│ async
▼
PassboxService / PassboxUtilities serial / concurrent background queues
│ blocking calls
▼
PassboxClient (UniFFI) ── Rust passbox-client ── HTTPS ── passbox-server
│ SecureStore callbacks
▼
AppSecureStore
"account" → Keychain (kSecClassGenericPassword, WhenUnlockedThisDeviceOnly, shared access group)
"cache" → App Group file with NSFileProtectionComplete, excluded from backups- PassboxKit contains the logic and is tested on Linux and macOS: the async
PassboxService(every Rust call runs on a background queue, never on the main actor), theSecureStoreimplementation, a keychain abstraction with an in-memory implementation for tests, biometric unlock, the@Observableview models, item drafts, search and filters, the Security Check report, the auto-lock timer, AutoFill matching by registrable domain and forgiving Secret Key input. The library is built in Swift 6 language mode with strict concurrency; the generated bindings are compiled in Swift 5 mode. - The app and the extension are thin SwiftUI layers. They build in Swift 5 mode with
SWIFT_STRICT_CONCURRENCY=complete, because they can only be compiled on macOS and the UIKit/AuthenticationServices overrides are simpler to keep warning-free there. - Storage. The account record contains the Secret Key, so it lives in the keychain, readable only while the device is unlocked and never synced or backed up. The offline cache contains only ciphertext from the server; it is a file in the App Group container with complete file protection. App and extension share both through the keychain access group
$(AppIdentifierPrefix)net.passbox.sharedand the App Groupgroup.net.passbox.ios. - Biometric unlock. Right after the first successful password unlock (sign-in, sign-up or unlock) the app asks "Unlock with Face ID from now on?" ([Yes] [Later]; "Later" pauses the offer for 30 days, and it is shown twice at most). It can also be turned on in Settings › Security. The app stores the client's unlock key (the derived master keys from
PassboxClient.unlockKey(), never the master password) in a separate keychain item withSecAccessControl(.biometryCurrentSet)andkSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. Unlocking with it (unlockWithKey) skips Argon2id, so it is fast and fits the AutoFill extension's memory budget. Enrolling another face or finger invalidates the item. Changing the master password in the app stores a new unlock key right away; a change on another device makes the next biometric unlock fail, and the item is removed. - Master password reminders (
BiometricPolicy, tested in PassboxKit). Face ID is not offered, and the lock screen says why, when the master password was not typed on this device yet (for example after reinstalling), every 14 days by default (Settings: every 14 days / every 30 days / never), and after the device restarted (the boot time fromsysctl kern.boottimediffers from the one at the last unlock). App and extension share these settings through the App Group. - Privacy. A cover hides the window whenever the scene is not active, so the app switcher snapshot shows no vault contents. The vault locks when the app returns from the background after the auto-lock timeout. The choices match the other apps (1, 5, 15, 30 or 60 minutes) plus "Immediately", which locks on entering the background; the default on mobile is 5 minutes. Copied values use
UIPasteboardwithlocalOnly(no Universal Clipboard) and expire after 90 seconds. Secure notes never show their text in lists ("Secure note · 3 lines"). - Layout. iPhone uses a floating bottom bar with four outline icons (Items, Security Check, Generator, Profile) and the raised round "+" in the middle; each tab keeps its own navigation stack alive, and search opens from the search pill on the home screen. iPad (regular width) uses a three-column
NavigationSplitView: sidebar (the account, All items, Favorites, categories, Passkeys, 2FA codes, the vaults with their colored dots by family or team, Security Check, Generator, Family & Team, Trash, Profile, a full-width Solar New item pill), list (selected row in the Sky selection tint with a marker) and detail. Keyboard shortcuts: ⌘F search, ⌘N new login, ⌘1/⌘2/⌘3 items, Security Check, profile, ⇧⌘O vaults, ⌘L lock, ⌘R sync, ⌘E edit, ⌘S save, ⇧⌘C copy password, ⌥⌘C copy username. Text is Plus Jakarta Sans in Dynamic Type sizes (Font.pb,Font.custom(_:size:relativeTo:); navigation bars throughUIFontMetrics), every icon button has a touch target of at least 44 × 44 points, animations are springs that Reduce Motion turns into fades, and light and dark mode use the Passbox Marina tokens and follow the system setting.
Languages
The app and the extension are available in English and German; German is used when it is the device's (or, under Settings › Passbox › Language, the app's) language. Wording follows packages/i18n/GLOSSARY.md and the shared catalog packages/i18n/messages/<lang>/common.json (formal "Sie").
- New UI text (the redesigned screens) is in PassboxKit's tables and shown with
tr("key")/Text(kit:), so both languages are tested on Linux. - SwiftUI text lives in
Shared/Localizable.xcstrings, a String Catalog that both targets include. SwiftUI literals (Text("Unlock"),Button("Cancel")) areLocalizedStringKeys, other UI strings useString(localized:). Interpolations become format specifiers ("Unlock with %@","%lld items"); counts use the catalog's plural variations. Building in Xcode adds new strings to the catalog (SWIFT_EMIT_LOC_STRINGS); translate them before committing. - Info.plist strings (Face ID and camera usage) are in
Passbox/InfoPlist.xcstringsandPassboxAutoFill/InfoPlist.xcstrings. - PassboxKit (errors, categories, strength warnings, auto-lock labels, the Emergency Kit, AutoFill warnings) uses
Resources/<lang>.lproj/Localizable.strings, because SwiftPM compiles String Catalogs only on macOS.L10n.string(key, ["name": value])fills named placeholders like the shared catalogs,L10n.plural(key, count:)picks<key>.oneor<key>.other. Tests force a language withL10n.$language.withValue("de") { … }and check that both languages have the same keys and placeholders.
Safety nets and account security
- Trash. "Move to Trash" (list swipe, context menu, item detail) shows a toast with Undo for 10 seconds. The trash (iPad sidebar, iPhone Settings) restores items or deletes them permanently, and "Empty Trash" asks first. Items trashed more than 30 days ago are purged after each online unlock (
purgeTrash). Trashed items never appear in lists, search, Security Check, AutoFill or the QuickType identities. - Previous passwords. The Rust client keeps the passwords an item had before; "Show Item History" lists them as earlier versions with Restore (which keeps the replaced password in the history).
- Settings › Account: Show Emergency Kit (master password first, then the kit), Devices (sign out one or all other devices), Activity (the last 50 events; failed sign-ins are called "sign-in attempts", with the note that wrong master passwords are detected on the device and may not appear) and Change Master Password (strength gate, passphrase suggestion, "other devices will be signed out, your Secret Key stays the same").
- Emergency Kit.
EmergencyKitbuilds the same content as the web kit: sign-in address, email and Secret Key (never cut off), a line for the master password, the creation date and three steps for a new device. The app shows it, prints it and shares it as a PDF drawn withUIGraphicsPDFRenderer(written to the app's temporary folder with complete file protection and removed when the kit closes). Instead of a "saved" checkbox, the user types the last 4 characters of the Secret Key. Until then a reminder stays at the top of the item list. Signing out (also "Use a different account…" on the lock screen) explains that the Secret Key is removed from the device and offers to show the kit first. - Errors.
errorMessage(_:context:)writes every error as "what happened and what to do". The unlock screen says "Wrong master password" and never mentions the email or Secret Key; sign-in checks the Secret Key's format locally ("2 characters are missing after P1");rate_limitedanswers show the wait in minutes. "Forgot master password?" (sign-in and unlock) explains honestly that nobody can reset it and lists the options. - Notices. Sign-up and Settings › About say that Passbox is a preview that has not been independently audited. Entries that could not be decrypted (
PassboxClient.decryptionErrors()) are listed in a notice above the items, with the technical text behind "Technical details", and are never deleted. Account ID, server address and algorithms are under Settings › About › Technical details.
Passwords and two-factor codes
- Show in large type (long press on a password, or the VoiceOver action) opens a full-screen view with one numbered box per character (digits in Sky blue, symbols orange), which works in landscape and keeps the screen on.
- Two-factor code (2FA). The editor field accepts the setup key or an
otpauth://link and explains problems before saving (totpInputProblem: a single 6-digit code instead of the key, or an invalid key). Scan QR Code uses VisionKit'sDataScannerViewControllerwhenisSupportedandisAvailable(camera permission is asked first); otherwise, or in addition, a screenshot from Photos is decoded with Vision (VNDetectBarcodesRequest). Google Authenticator exports (otpauth-migration://) get their own explanation. - Strength. Master passwords must be
acceptableMasterPassword(score 3 or more and at least 12 characters); the meter shows the core's reason in the user's language, and "Suggest a password made of words" creates a passphrase. The generator has a Words mode (EFF word list, 3–10 words, separator, capitals, digit), and "Avoid similar characters" is on by default. - Security Check is described under Screens.
AutoFill
- The extension (
PassboxAutoFill) reads the same account and cache as the app. It runs in its own process and never inherits the app's keys, so the user unlocks with the master password or Face ID / Touch ID. prepareCredentialList(for:)lists logins whose website has the same registrable domain as the requested service (exact host first), then all other logins with search. Matching uses a built-in subset of the Public Suffix List, soexample.co.ukandother.co.uknever match.- Picking a login that does not match the requesting app or website asks first (
CredentialMatcher.fillWarning): "bank.example is saved for this login, but you are filling evil.example. Only continue if you trust this app." [Cancel] [Fill Anyway]. A lookalike app or site cannot get a password with a single tap. - After sign-in, a one-time card in the item list ("Fill passwords automatically") shows the steps for the running iOS version (iOS 17: Settings › Passwords › Password Options; iOS 18: Settings › General › AutoFill & Passwords), opens the right settings page (
ASSettingsHelperon iOS 18) and switches to "Done" onceASCredentialIdentityStorereports Passbox as enabled. - After every sync the app replaces the identities in
ASCredentialIdentityStore(domain, username, item ID; no passwords), so iOS shows Passbox suggestions in the QuickType bar. Choosing one callsprovideCredentialWithoutUserInteraction(for:), which fails withASExtensionError.userInteractionRequiredwhile locked; iOS then callsprepareInterfaceToProvideCredential(for:), the user unlocks and the login is filled. - Items are saved with their
extraJsonunchanged, so data the app does not show (for example passkeys created in the browser extension) survives edits on iOS.
Families, teams and sharing
The cryptography (key pairs, signed member lists, sealed vault keys, the key directory) is in the Rust core; see architecture/sharing.md. The app only shows what the core verified and calls its operations. Logic and wording live in PassboxKit (Sharing/SharingTypes.swift, Sharing/SharingSupport.swift, Models/VaultModel+Sharing.swift), so they are tested on Linux; the SwiftUI screens are in Passbox/Views/Sharing.
- Vault list.
VaultModel.sharing(organizations, shared vaults, warnings) is loaded with every snapshot.vaultSectionsgroups personal vaults first, then one section per family or team (families first, then by name) with an SF Symbol (figure.2.and.child.holdinghands,person.3); the iPad sidebar and the iPhone filter menu use it. Items of shared vaults carry a badge with where they live ("Doe family" for the family vault, "Customers · Sales" otherwise), also in AutoFill. New items go to the personal vault unless another writable vault is picked in the editor, which says who will see the item. - Item details show Lives in and Who can see it ("You, Anna and 2 others", from the verified member list: people by email, groups by name), which opens the vault's access list. With view-only access (
view, an unverified member list, or no key on the device) the item hides Edit, favorite, Move to Trash and Restore and explains: "You can view and copy this item, but not change it. Ask a manager of ‘Customers · Sales’ for edit access."VaultModel.saveandmoveToTrashrefuse such items too (InputError.viewOnly), and server answers (403) are mapped to the same kind of message. - Family & Team (Settings, and the iPad sidebar with a badge for members waiting for confirmation): list of organizations; Create Family (with the family vault's name) and Create Team; Accept Invitation…. An organization shows members with role and status ("Waiting for confirmation", "Paused", a warning icon when a key changed), pending invitations with Revoke (confirmation), shared vaults with this account's role, New Shared Vault (admins), Leave Family/Team with what it means (owners cannot leave), and the organization code under Technical details.
- Inviting takes an email and a role (owners can make admins, admins invite members and guests; each role is explained). The result is the link: Send Link… (share sheet with subject and message) and Copy Link; the screen says the server mails it when it can and that the link should travel through a trusted channel.
- Confirming looks the member's key up in the key directory (
lookupKey, which pins it) and shows their security code in large groups of five digits. Comparing is optional: Codes Match – Confirm, Confirm Without Comparing (with a footnote on what that trusts) or Codes Don't Match (explains not to confirm). In a family the member then gets the family vault. - Members: role picker, the member's security code, Remove from Organization with the consequence ("loses access right away; the vaults get new keys; what they saw may have been copied; change important passwords"). Vault access (managers): add a confirmed member with a role, change roles (context menu), remove access (the key is rotated), and Renew Key Now when
rotationPendingis set. - Accepting an invitation. Invitation links point to the server (
https://<server>/invite/<token>#org=<code>); self-hosted servers have no fixed domain, so there are no universal links. The app registers the URL schemepassbox://invite?link=<https link>(alsopassbox://invite/<token>#org=…) and has a paste field that finds the link in a pasted mail. Flow: preview (family or team, invited email, role and what it may do, expiry) → Join → "Waiting for confirmation" with a pointer to the own security code. A link for another server or another email says so; a link without#org=warns that the organization cannot be checked. A link opened while locked or signed out is shown after unlocking. - Warnings (
sharingWarnings: changed keys, unverifiable organizations, member lists or keys, role mismatches) appear as a banner above the items ("1 security warning for sharing") with a details sheet: title and plain explanation per kind, the core's technical text, and for a changed key Compare and Trust New Code…, which shows the new code to compare beforeacceptKeyChange. - Security code: Settings › Your Security Code shows the own fingerprint (copyable, read out group by group by VoiceOver) and Check With the Server (
verifyOwnKey). - Tests:
SharingTests(links, codes, grouping, roles, warnings, errors in both languages) andSharingIntegrationTests(two accounts against a real server: family with invitation, preview, accept, confirmation with matching codes, shared editing, AutoFill candidates, a team with a view-only vault, removal and leaving).
Screens
The app uses Passbox Marina, the design system of every Passbox surface (design/system.md, design/brand.md): dark first with a light theme of equal care (the system setting decides), deep Marina surfaces, Solar Silk as the one signature accent and Sky Horizon for selection and information, generous radii, the stacked-card lip, Plus Jakarta Sans and the Lucide icon family instead of SF Symbols (system controls such as the share sheet keep theirs).
- Tokens.
Shared/DesignTokens.swiftis generated frompackages/ui/tokens/tokens.jsonin passboxnet/core (colors for light and dark without asset catalog entries, type scale, radii, spacing, motion, gradients); never hard-code a color.Shared/Theme.swiftmaps them for the app:Palette(=DesignTokens.Colors),Color.linkTint/controlTint/swipeAction/canvas/card,Font.pb(_:_:)(Plus Jakarta Sans at the size of a system text style, scaled with Dynamic Type) andPassboxFont.applyToUIKit()for navigation bars and segmented controls. Text in a status color uses the*Texttokens (4.5:1), fills and icons the plain ones. - Colours. Solar Silk (
accent) is a fill, always with Marina ink (accentContrast), never white on Solar and never Solar text on white; it is reserved for primary pills (.pill(.primary), the sidebar's New item), the raised "+" and the prominent Edit glass pill. SwiftUI's.tintcolours links and bar buttons as text, so the app and the AutoFill extension are tinted withColor.linkTint(accent-text: deep Sky #1E5F7D in light, Solar in dark), and theAccentColorasset (the global accent for UIKit and system alerts) has the same values; checked controls (toggles, sliders, the two-factor countdown ring) get.tint(.controlTint)(control: Marina in light, Solar in dark; iOS draws the toggle thumb white). Swipe actions draw white labels on their tint, so they useColor.swipeAction(Nocturnal Marina) andColor.swipeFavorite(the Sky lip). Selection (the iPad list, search results and sidebar, the bottom bar's tab) isSelectionBackground: the Skyselectiontint with a 3-pointselectionMarkerbar (Marina in light, Solar in dark) and the normal text colours; the sidebar replaces the system's solid tint fill with it. Focus rings usefocus. Warning is orange and never Solar. The mark, the avatar and the Face ID button put Solar on the Marina signature gradient; the backdrop is deep Marina with a Sky glow at the top and a faint Solar glow in the lower right. The Emergency Kit PDF uses fixed Marina for its eyebrow, since paper is always white. The launch screen usesLaunchBackground(=canvas). - The lip.
Lip,accentLip(_:)andcardLip(_:)inShared/Components.swiftdraw the static darker bottom edge of the palette reference: 2 points ofaccentEdgeunder Solar pills and the raised "+" (a pressed pill sits down onto it), 3 points ofcardEdgeunder stat tiles and the home screen's category cards,marinaLipunder the empty-state tile. The space is reserved below the view, it is never animated, and Reduce Motion changes nothing. - Icons.
Shared/PassboxIcon.swiftandShared/Icons.xcassetsare generated frompackages/ui/brand/icons.jsonin passboxnet/core (template SVGs, 24 × 24, 2-point stroke).PBIcon(.keyRound, size: 20)scales with Dynamic Type;Image(pb: "key"),Label(_:pbIcon:)andIconTile(symbol:)take the SF Symbol names that PassboxKit uses (ItemCategory.symbolName…) and map them to Lucide (PassboxIcon.forSymbol), falling back to the SF Symbol only where Lucide has no counterpart. - Signature components (
Shared/Components.swift, shared with the AutoFill extension):IconTile, pill buttons (.buttonStyle(.pill(.primary)),ChipButtonStyle),StatTile,AlertCard,ScoreRing,SectionHeader,EmptyStateView(the Solar icon on a Marina tile inside dotted rings),SelectionBackground, the lip modifiers,passboxField(),BrandBackdrop(backdrop gradient, Sky and Solar glows, grain and the dotted grid with corner dots),BrandArt(the abstract onboarding illustration around the keyhole mark) andTileLabelStylefor settings rows.LogoMarkdraws the Solar keyhole on the Marina signature gradient,LogoLockupadds the wordmark. The app icons (Passbox/Assets.xcassets/AppIcon.appiconset: light, dark and tinted) are generated byscripts/generate-brand-assets.mjsin passboxnet/core. Item tiles (Shared/Tiles.swift) put real favicons on the lighticon-tilesurface with a 28 % radius, or a tinted monogram. - Glass. On iOS 26 the floating controls (bottom bar, the buttons of the item details) use Liquid Glass (
glassEffect,GlassEffectContainer, behindif #available(iOS 26, *)and#if compiler(>=6.2)so older Xcode versions still build); iOS 17 and 18 get.ultraThinMaterialwith theglasstoken and a light edge (Passbox/DesignSystem/Glass.swift). Forms and grouped lists sit on the canvas with surface rows (passboxListBackground()). Haptics mark results (copied, saved, moved to the trash, an error), never plain navigation.
Signed out and locked
- Onboarding: the signature backdrop with the dotted grid, the brand illustration, "Your passwords. Safe with you." and the pill Get started (create an account) with I already have an account · Sign in. Devices that were signed in before go straight to the sign-in form. The form keeps the server address, email, master password (with strength meter and word suggestions) and Secret Key, Use a Code from Another Device and a pill submit button.
- Lock screen: the backdrop with the unlock form on a bezelled panel: account card with avatar, a round Unlock with Face ID button (Solar glyph on the Marina signature gradient, prompted right away), the master password field and the pill Unlock.
Items
- Vault switcher (top left, the current vault's colored icon): all vaults, personal vaults, then shared vaults by family or team, each with icon, color and item count; swipe left to rename (personal vaults) and choose one of 14 icons and 10 colors; "New Vault". The choice is remembered. Without a choice, the first personal vault is a blue house, family and team vaults get their symbol, other vaults a stable color.
- Home (iPhone): a big bold title (the vault or filter), "142 items · all synced", the search pill, 2 × 2 category cards (Logins, Cards, Notes, Identities with their counts; a tap filters the list, a chip removes the filter) and Recently used (the last three items used, round tiles, how long ago), then all items. Notices (Emergency Kit, decryption problems, sharing, the AutoFill setup card) are cards at the top; the empty state is a list row below them, so it never overlaps the AutoFill card.
- List: sections by recency (Today, Yesterday, Last week, Last month, then by month) or by letter for A–Z / Z–A. Rows show the website icon or a tinted two-letter monogram ("EU" for EuroDNS, "DB" for Deutsche Bank; logins of the same site share a color), the title and username, email or a safe summary (card ending, note length). Favorites have a star, risky logins (breached, weak, reused, http) a shield badge, shared items a badge with their family or team vault.
- "⋮" menu: Select Items (multi-select with a floating bar: favorite, move or copy to another vault, move to the trash with one Undo for all), Show (All, Favorites, each category, Passkeys, 2FA codes, each with its count) and Sort By (Newest, Oldest, A–Z, Z–A, Recently used).
- Rows: swipe right to favorite, left to move to the trash or copy the password; long press for copy, open website, favorite, move/copy to a vault and trash. Pull to refresh syncs. While the first snapshot loads, skeleton rows are shown; empty vaults and empty filters have their own message and action.
- Create (the raised "+" in the bottom bar on iPhone, the sidebar button and the toolbar on iPad): login, secure note or credit card, in the vault the list shows if it is writable.
- Search (the search pill): all vaults, ranked (title start, title word, title, other fields, near matches), case-, accent- and typo-tolerant ("gihtub" finds GitHub, one typo from 4 letters, two from 8), with scopes All, Name, Username, Website and Notes. Without a query it shows recently used items. Nothing leaves the device.
Item details
On iPhone the details open as a sheet. The top bar has collapse (chevron down), Edit, Share and "⋮" (copy password or username, favorite, move or copy to a vault, item history, move to the trash). On iPad the same actions are in the toolbar.
- Security Check card (only when the login has problems): "Password used more than once · No two-factor code" in one line, problems in orange; tap to expand explanations with a fix button each: Change Password opens the editor with the generator, Set Up Two-Factor Code opens the QR scanner, Switch to https:// fixes the address in one tap.
- Header: large tile, title, a category pill and the vault with its colored dot.
- Field cards: username, password and two-factor code in one card, websites in another, then passkeys, notes, custom fields and tags. The password label carries its strength ("Password · Strong", colored, with a shield icon). Tapping a row copies the value (haptic and toast; the clipboard is local to the device and expires after 90 seconds); hidden values have a reveal button; a long press offers copy, show, large type and open. The two-factor code shows a countdown ring and turns red in the last 5 seconds. VoiceOver reads each row as one element with "Copies the value" and actions for reveal, large type and open.
- Shared items show who can see them (opens the vault's access list); view-only items explain why they cannot be changed.
- Metadata card: Last filled (AutoFill on this device), Last used (a copy after that), Last changed and Created, each as "Yesterday at 17:39 (1 day ago)" or "Sunday, 11 January 2026 at 11:26 (9 months ago)", refreshed every minute. Dates that are not known are left out, never guessed. Show Item History lists versions newest first with diff-style changes (removed values struck through in red, new ones in green, secrets masked until revealed) and Restore This Version.
- Share on a personal item offers to move it into a shared family or team vault (saying who will then see it) or to set up Family & Team; on a shared item it opens the vault's access list.
Editor
Type-specific sections with icons (the same QR scan screen as below for two-factor codes): name with a live tile preview and the vault picker (colored icons), username, password with strength meter, reveal and the generator sheet (password or words, length, digits, symbols, similar characters; Use Password), website with the one-tap https fix, two-factor code with QR scanning and help, card fields, custom fields (text, hidden, email, website; edit labels, delete, Reorder with drag handles), notes and favorite. Cancel with unsaved changes asks first; ⌘S saves.
Security Check
The score ring with the rating ("Room for improvement") and "5 of 7 logins are secure" (what costs points folds out), an alert card for breached passwords ("We found 3 passwords in data breaches · See details") or logins at breached websites, and the report as four big-number tiles: At risk (breached passwords and logins at breached websites, VaultModel.atRiskLoginIDs), Reused, Weak and Secure; tiles with problems open their list. Then the breach check card (opt-in: "Check for breached passwords" explains the k-anonymity steps before it is turned on: only the first 5 characters of a password's SHA-1 hash leave the device; then "No breaches found" in green or the number of breached passwords, with the time of the last check); and Password security rows with icon, what to do and a count badge: Weak passwords, Compromised passwords (only after the opt-in), Reused passwords, Missing two-factor codes, Unsecured websites. Each row opens a list (reused passwords grouped by password) with a fix button per login and, for unsecured websites, Switch All to https://. Pull to refresh syncs.
With the core's security analysis (VaultModel.securityAnalysis, refreshed with the items and after breach checks) the ring shows the score 0–100 with its rating and what costs points ("−18 Similar passwords"), followed by Fix These First (up to five items with their problems and the points they gain) and More Checks: similar passwords, lookalike addresses, accounts in breaches, expired and expiring cards and documents, old passwords, passkeys available, duplicates, logins without website or username, and both factors in one item. Each opens a list with the details and instructions; swiping (or the accessibility action) dismisses a finding or shows it again. Dismissals are stored in the item and synced. See security-analysis.md.
Breached websites ("Breached website"): logins whose site had a breach published after the password was set show "Netflix had a data breach on Oct 21, 2026 (passwords, email addresses). Your password is older. Change it now." with a link to the site's change-password page, the facts of each breach (date, data, accounts, your affected and fixed logins), Have I Been Pwned as the source and "Notify me about new breaches". The count is added to the Security Check tab badge. The app checks the catalogue at unlock and when it becomes active, and registers a BGAppRefreshTask (net.passbox.breach-catalog, UIBackgroundModes: fetch) every ~6 hours. Limits: iOS decides when background refresh runs (usage, Low Power Mode, the user's Background App Refresh setting); with a locked vault the task only downloads the catalogue (matching needs the decrypted items), and the notification follows at the next unlock.
Profile
- Account card (round avatar with Solar initials on the Marina signature gradient, email, server, "Secret Key stored on this device"), the same alert card and report tiles as the Security Check (a tap opens it), and Sign In on Another Device: shows a code and a QR code (
passbox-link:v1?server=…&code=…, as in the web app) valid for 10 minutes, waits for the new device, then asks "Is this your device?" with its name and IP address before sending the email and Secret Key sealed to it (DeviceLinkModel,device_linkin the core of passboxnet/core). The master password is never sent. On the new device, the welcome screen has Use a Code from Another Device (type or paste the code, scan the QR code in Passbox's scan screen, or scan it with the Camera app, which opens Passbox through thepassbox-link:URL scheme); email and Secret Key are filled in and only the master password is typed. - Stats chips (logins, notes, cards, identities, passkeys, 2FA codes, shared items), scrolling horizontally; tapping one opens the item list with that filter.
- Security: Unlock with Face ID / Touch ID (or master password only), Use device passcode if Face ID fails (stores the unlock key with
.biometryCurrentSetor.devicePasscode; off by default and explained), master password reminders, Auto-lock, Lock Now. Fill passwords automatically: setup steps and Suggestions above the keyboard (off removes all QuickType identities). Account: Devices, Activity, Change Master Password, Emergency Kit, Recovery (status of the recovery code and organization recovery), two-factor sign-in status, security code. Then Family & Team, vaults (icons and colors), trash and generator (iPhone), Appearance (Show website icons, language), About (version, security design, source code) and Sign Out.
QR scan screen
One screen for two-factor setup (the editor, the Security Check's Set Up) and for signing in with a code from another device: the camera (VisionKit's DataScannerViewController) on a rounded viewfinder with Solar corner marks and a glowing Sky scan line that moves (still with Reduce Motion), a hint card and Choose a Screenshot (Vision decodes the picture). Without a camera (simulator, access denied) the viewfinder shows the QR mark and the hint says how to allow access.
Item extras
The details, history, Security Check and list use the Rust client's item extras (item-extras.md); PassboxService wraps each call, and the slow network calls (icons, breach checks) run on a detached task instead of the client's serial queue so they never hold up unlock, sync or saving.
| Feature | In PassboxKit | Rust client |
|---|---|---|
| Created date | VaultModel.createdDate(of:) (hidden when 0) | Item.createdAt |
| Item history | VaultModel.history(of:) → HistoryVersion (device names from sessions(); versions this device cannot decrypt say so and cannot be restored); without server versions, earlier passwords | itemHistory(id:) |
| Website icons | CachedWebsiteIcons over PassboxService.icon(domain:), only with Show website icons on; monograms otherwise | icon(domain:), cached 7 days in cache_icon_* (files, not the keychain) |
| Breach check (opt-in) | ServiceBreachChecker → BreachReport | breachedItems(), breachCheck(password:) |
| Last filled / last used / opened | VaultModel.usages, recordUse(itemID:kind:) (copy toast, opening the details); the AutoFill extension leaves only the item ID and time in AutofillHandoff (App Group defaults), which the app records after unlocking and clears | recordItemUse, itemUsages(), stored encrypted under cache_usage |
| Per-item Security Check card | VaultModel.itemHealthIssues(of:), the vault-wide result until it answers | itemHealth(id:) |
| Bulk favorite and trash | VaultModel.setFavorite(_:itemIDs:), moveToTrash(itemIDs:) (view-only items are counted as failed first) | setFavorite(ids:favorite:), trashItems(ids:) |
Still placeholders: moving items between vaults (VaultModel.move(itemIDs:toVault:) copies into the target vault and moves the original to the trash) and the vault icon and color, which are stored per device in AppSettings.vaultAppearances rather than in the encrypted vault metadata.
Two-factor sign-in
When the account has two-factor sign-in on, sign-in, unlock (password, Face ID / Touch ID, also in the AutoFill extension) and a master password change that needs a fresh login stop after the password with VaultModel.twoFactorPrompt. TwoFactorPanel (Shared) asks for a code from the authenticator app or a recovery code (recognized by its length) with "Remember this device for 30 days"; completeTwoFactor(code:rememberDevice:) finishes the login. Security keys are bound to the web vault's domain, so the app cannot use them; accounts with only security keys see a hint to use the web app or a recovery code. Enrollment (authenticator app, security keys, recovery codes) is only possible in the web app; Settings shows a hint. Logic and tests live in PassboxKit (TwoFactorIntegrationTests).
Limitations
- AutoFill memory. iOS limits AutoFill extensions to a much smaller memory budget than apps. Face ID / Touch ID unlock avoids key derivation entirely (see biometric unlock). Unlocking the extension with the master password still runs Argon2id at the account's parameters (128 MiB for new accounts), which may exceed the budget on some devices; this has not been measured on devices yet. Biometric unlock is the safer choice in the extension.
- One-time codes in AutoFill (
ASOneTimeCodeCredential, iOS 18) and passkeys (ASPasskeyCredential) are not implemented yet. Passkeys stored by other clients are kept intact (extraJson) and shown as a hint in the item details. - Public Suffix List. Domain matching uses a built-in subset of the list. The full list is not bundled.
- Secrets in Swift memory. The master password passes through Swift
Strings, which cannot be wiped reliably. Keys stay in Rust, where they are zeroized on lock. - Locking in the background. With a timeout longer than "Immediately", the vault stays unlocked in memory while the suspended app is in the background and locks when it returns after the timeout.
- Sharing: groups (creating them, managing their members) and suspending members are only available in the web app; groups already shared with a vault are shown by name. Universal links for invitations need a fixed domain, so invitations open through the
passbox://scheme or by pasting. Signing up from an invitation on an invite-only server needs the web app (the FFI's sign-up takes no invitation token yet). - Not in the app yet: creating identities and other categories (they can be viewed and edited), an in-app camera scanner for device codes (the Camera app opens
passbox-link:QR codes in Passbox), setting up a recovery code (web app), alternative app icons and a Caps Lock hint (iOS reports Caps Lock only for hardware keyboards). - Item extras (see Item extras): the app does not use server-side moves between vaults yet, and vault icons and colors do not sync.
- The app and extension compile only on macOS; their code is verified in CI, while PassboxKit (all logic) is also built and tested on Linux.