This repository is the native Android client: Kotlin, Jetpack Compose with Material 3, minSdk 28 (Android 9), targetSdk 35, application ID net.passbox.android (the Kotlin namespace stays dev.passbox.android). All cryptography, networking, sync and the offline cache come from the Rust client of passboxnet/core (crates/client) through UniFFI bindings (crates/ffi). The app has no cryptographic code of its own; it only uses the Android Keystore to protect what the Rust client stores on the device.
Architecture
Compose UI (ui/*) Autofill (autofill/*)
│ StateFlow │ PassboxAutofillService, AutofillAuthActivity
▼ ▼
ViewModels ──────────► VaultRepository (core/PassboxVaultRepository)
│ every call on Dispatchers.IO, serialized by a mutex
▼
UniFFI Kotlin bindings (generated, JNA)
│
▼
libpassbox_ffi.so (passboxnet/core: crates/ffi → crates/client → crates/core)
│ SecureStore callback
▼
EncryptedFileStore: AES-256-GCM, non-exportable Android Keystore key,
ciphertext in noBackupFilesDir/vault/<key>.bin| Package | Contents |
|---|---|
dev.passbox.ffi | PassboxAndroid.ensureInitialized(context): loads the library and initializes TLS certificate verification (see below) |
core | VaultRepository (session state, items, trash, devices, activity, all Rust calls), EncryptedFileStore + KeystoreAead (the SecureStore), BiometricVault, MasterPasswordReprompt, AutoLock, SecureClipboard, AppSettings, AppLocale (per-app language), UiText and error messages, EmergencyKitPdf |
model | Pure Kotlin: item fields and localized labels, search with typo tolerance (Search), sort orders, recency and letter sections and filter counts (ItemList), dates (DateText), the security check per item (Health), vault icons and colors (VaultAppearance), passkey summaries, the editor form (keeps unknown data, custom fields), linked Android apps (LinkedApps), server address validation |
ui | Compose screens and their ViewModels: MainShell (tabs, screen stack, two panes), onboarding with sign-in and sign-up, Emergency Kit, unlock, item list, search, item detail (large type, previous passwords, linked apps, item history), editor with QR scanner, custom fields and generator sheet, trash, generator (password, words, PIN), security check with drill-downs, profile (devices, activity, master password, recovery code, two-factor sign-in, sign in on another device, help), family & team (ui/sharing); shared building blocks in ui/components (Signature.kt for the brand's signature components, Expressive.kt, Components.kt, SharedElements.kt); the theme in ui/theme (Tokens.kt generated, Theme.kt, PassboxIcons.kt generated) |
autofill | Field detection (FormParser), domain and app matching (Matching, AppSignatures), responses, the service and its activity |
AppContainer (created by PassboxApplication) holds the process-wide objects; the autofill service runs in the same process and shares the unlocked session. ViewModels get their dependencies through small factories (ui/ViewModels.kt), so they can be tested with fakes.
Native library and bindings
scripts/build-native.sh (run by the Gradle task :app:buildRust before every build) builds the Rust code of passboxnet/core, checked out next to this repository (or set PASSBOX_CORE_DIR). It does two things:
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 --platform 28 build -p passbox-ffi --releaseintoapp/build/rust/jniLibs.- A host build of
passbox-ffi, thenuniffi-bindgen generate --library … --language kotlinintoapp/build/generated/uniffi/kotlin(library mode, so the bindings always match the Rust code; they are not committed).
The bindings use JNA (net.java.dev.jna:jna@aar, which brings libjnidispatch.so). crates/ffi/uniffi.toml in passboxnet/core renames the message field of error variants to detail for Kotlin, because a field named message clashes with Throwable.message.
Rust's HTTPS client verifies certificates with rustls-platform-verifier, which uses Android's trust manager through JNI. It needs the app context once per process, so crates/ffi/src/android.rs in passboxnet/core exports Java_dev_passbox_ffi_PassboxAndroid_initialize, which the app calls on startup. Its JVM half (the org.rustls:rustls-platform-verifier AAR, MIT/Apache-2.0) is vendored in third_party/maven; the build checks that its version matches rustls-platform-verifier-android in the Cargo.lock of passboxnet/core.
Languages
All user-visible text is in res/values/strings.xml (English) and res/values-de/strings.xml (German, formal "Sie"), with <plurals> for counts. Terms follow packages/i18n/GLOSSARY.md in passboxnet/core; shared messages (strength warnings and suggestions, two-factor input problems, errors such as error.rateLimited, the preview notice) use the wording of packages/i18n/messages/{en,de}/common.json. German is used automatically when the system language is German.
- Per-app language: Settings › Appearance › Language (system language, Deutsch, English). On Android 13+ this uses
LocaleManagerandres/xml/locales_config.xml, so the choice also appears in the system's app settings. On Android 9–12 it is stored in preferences and applied byAppLocale.wrapinattachBaseContextof both activities and the autofill service. - ViewModels return
UiText(a string or plural resource with arguments) instead of text, so they stay testable on the JVM and the UI resolves them in the current language. - Checks:
StringResourcesTestfails when a German string or plural is missing, has different placeholders, or uses "du"; it also scans the Kotlin sources and fails on text literals inText(…),contentDescription,label/titlearguments, snackbars and toasts. - Field labels stored inside items stay in English (
Username,Password, …), as on the other clients; the UI shows translated labels for well-known field IDs.
Design and navigation
The app uses Passbox Marina, the design system of every Passbox surface (design/system.md, brand line in design/brand.md), on top of Material 3 components:
Theme (
ui/theme/Theme.kt):PassboxThemetakes the generatedTokens.kt(frompackages/ui/tokens/tokens.jsonin passboxnet/core): Material color schemes for light and dark, the full palette asPbColors(LocalPassboxColors), Plus Jakarta Sans (res/font) with the type scale (PassboxType, mapped onto Material's roles), and the 12/20/24/28 dp shapes. Dark-first with a light theme of equal care; it follows the system unless Profile › Appearance says otherwise. Material You colors are an opt-in there and off by default, because Marina, Solar and Sky are what make Passbox recognisable; the opt-in replaces only Material's scheme, the signature components keepPbColors.LocalReducedMotionis true when the system turned animations off (shimmer, scan line and springs then stand still).Colors (Passbox Marina, design/brand.md): Nocturnal Marina
#243E4Bis the base, Solar Silk#FFE26Dthe signature accent, Sky Horizon#6EABC7the secondary. Rules on Android:- Solar is a fill with Marina ink (
accent+accentContrast), never white on Solar and never Solar text on light surfaces. Solar fills: the primaryPillButton(each screen's main action, e.g. Get started, Unlock, Save, Edit, Confirm, Done), the raised "+", the swipe-to-copy background and the torch button in the scanner. - Checked controls (switches, slider, progress bars, check circles in multi-select, radio buttons) use
control/controlContrast: Marina with white in light, Solar with Marina ink in dark. Material'sprimaryiscontrol, so plain MaterialButtons,TextButtons and radio buttons follow the same rule; they are used for secondary actions only. - Selection is Sky: the open item in the tablet list, the selected vault in the switcher, the active tab in the bottom bar and the rail, and selected filter chips use
selectionwith the normal text colors and a marker inselectionMarker(Marina in light, Solar in dark): a 4 dp bar at the start of the list row, the icon of the tab, the check of the vault. - Links and accent text use
accentText(deep Sky#1E5F7Din light, Solar in dark); digits in passwords too. - Status colors stay apart from Solar: the update banner and the Reused/Weak tiles are orange (
warning), never yellow. - On the backdrop (onboarding, unlock, Emergency Kit), which is dark Marina in both themes,
OnBackdropgives the dark theme's text colors and Solar links; the scanner viewfinder usesPassboxPalette(deep Marina, Solar scan line) for the same reason. - The lip (the stacked-card motif):
Modifier.lip(color, shape, depth)draws a static darker edge below a shape, inside the element's bounds. The primary pill and the raised "+" sit on a 2 dpaccentEdgelip that disappears while pressed (and when disabled);StatTileandCategoryCard(PbCard(stacked = true)) on a 3 dpcardEdgelip; the floating tiles of the brand illustration on a Marina lip. No animation, so reduced motion changes nothing; it is not used on text fields, list rows or dialogs. - Outside Compose: the window background before the first frame and the Android 12+ splash use the
canvascolor (res/values*/colors.xml,values*-v31/themes.xml) with the adaptive launcher icon (Solar keyhole on the Marina gradient with a Sky glow); autofill suggestions showic_passbox(Solar keyhole on a Marina tile, readable on light and dark keyboards); breach notifications use theic_notificationsilhouette with Marina as accent color; the Emergency Kit PDF prints in Marina and the light theme's text colors.
- Solar is a fill with Marina ink (
Icons: only the Lucide family of the brand (
res/drawable/pb_icon_*.xml,PassboxIcons,pbIcon(PassboxIcons.Copy)), no Material icons (the app does not depend onmaterial-icons-extended). To add an icon, add its Lucide name topackages/ui/brand/icons.jsonin passboxnet/core and rerunscripts/generate-brand-assets.mjsthere.Signature components (
ui/components/Signature.kt):BrandMark/BrandLockup,SignatureBackdrop(Marina backdrop gradient, Sky glow from the top, soft Solar glow lower right, dotted grid with corner dots),BezelPanel,BrandIllustration(mark in orbit rings with a Sky glow and floating tiles on a Marina lip; no people),PillButton(primary: Solar with Marina ink and the lip),RaisedPlusButton(Solar, Marina "+", lip, soft glow, canvas-colored ring),ToneChip,StatTile/StatGrid(stacked with the lip),AlertCard,ScoreRing,LargeTitle,SectionHeading,SearchPill,CategoryCard(stacked with the lip),UpdateBanner(the orange "Time to change your password. · Change now"),PbCard,PbEmptyState,TonePill,PbSwitch(controltrack),OnBackdrop,Modifier.lip. Favicon tiles (ItemTile) sit on the lighticon-tilecolor with a hairline ring, 28 % corners (round in "Recently used").Shell (
ui/MainShell.kt): four tabs in a floating bottom bar with outline icons (Items, Security, Tools, Profile; selected: the icon in the marker color on a Skyselectionpill, a count on Security for breached websites) and the raised round Solar "+" with its lip in the middle, which opens New item. Screens are pushed on a stack (saved across rotation, IDs only) and slide in horizontally. Predictive back (PredictiveBackHandler,enableOnBackInvokedCallback) scales the top screen with the gesture; back also ends multi-select and closes the detail pane. The item tile flies from the list to the detail header (shared element transition,SharedTransitionLayout). Edge to edge everywhere.Large screens (window ≥ 720 dp: tablets, unfolded foldables, landscape): a navigation rail (mark, raised "+", search, the four tabs with labels), the list (400 dp) and the selected item side by side; the open item is the row in the Sky
selectiontint with a marker bar at its start (normal text colors).Items: vault switcher (colored vault icon, name, chevron; a sheet with all vaults, personal vaults and each family and team, with counts) and "⋮" at the top, then the large title (Items or the vault's name) with "11 items · all synced" (or offline), the search pill, 2 × 2 category cards (Logins, Credit cards, Secure notes, Identities with counts; a tap filters), Recently used (the three most recently used items with round tiles and "20 minutes ago"; All sorts by last use) and All items. "⋮" has Select items, Show (type filters with counts: all, favorites, logins, notes, cards, identities, passkeys, with 2FA code; empty types hidden), Sort by (Newest, Oldest, A–Z, Z–A, Recently used; remembered) and the trash. Sections by recency (Today, Yesterday, Last week, Last month, then months) or by first letter. Rows: website icon or a tinted two-letter monogram, title, username, favorite and passkey marks. Swipe right copies the password (or marks a favorite), swipe left moves to the trash (with Undo); long press opens a menu (copy username, password or card number, favorite, edit, select, trash); TalkBack gets the same actions as custom actions. Multi-select with a floating action bar: favorite, copy to another vault, trash (undo restores all). Pull to refresh, a shimmer skeleton while the first sync runs, empty states with a call to action.
Search (the search pill on Items, the rail on tablets): searches title, username, website, card holder and tags of the shown vault; every word must match, small typos are allowed (optimal string alignment distance 1 for 4–7 letters, 2 from 8), accents are ignored, and title matches rank first. Without a query it lists recently used items. Note contents are never searched.
Item detail: top bar with back, Edit pill, share (the vault's access list for shared items, otherwise Family & Team) and "⋮" (favorite, show large, item history, trash). Header with the large tile, title and vault; for a weak, reused or breached password the orange banner Time to change your password. · Change now (opens the fix). The item's own Security check card ("Easy to guess · 2FA missing"), expandable with an explanation and a fix per problem. Fields grouped in cards (each with copy and, for secrets, reveal buttons) (sign-in data; card; websites; custom fields) with icons; the password label carries its strength ("Password · Strong", colored, shield icon). Tap a row to copy (haptic, snackbar, the clip is marked sensitive with
EXTRA_IS_SENSITIVEand cleared after 90 s); long press for copy, large type and reveal; the eye button reveals (hidden again after 30 s). Two-factor codes with a countdown ring, passkeys (read-only), notes, tags, previous passwords, linked apps, and a metadata card: Last autofill, Last used, Last changed, Created as "Yesterday at 17:39 (1 day ago)" or "Sunday, 11 January 2026 at 11:26 (9 months ago)", and Show item history (versions with what changed and Restore).Editor: live tile preview, fields per type, password with strength and a generator sheet (Use), QR scanner for 2FA, custom fields (text, hidden text, website, email, 2FA code) that can be renamed, reordered and removed (new fields get a random UUID as ID, like the web app), notes, favorite.
"+" (raised button in the bottom bar or rail): a sheet with the item types and the generator.
Vault icons and colors: the palette button next to a vault in the switcher (personal vaults, or shared vaults this account manages) picks one of 12 icons and 8 colors. They are stored in the vault's encrypted metadata next to its name (
icon,color;PassboxClient.setVaultAppearance), so the server sees nothing; vaults without a choice get a house (personal) or people (family, team) and a color derived from the vault ID. Renaming keeps unknown metadata keys.Security tab: large title with Check again, the score ring (0–100, with the rating and "Security score out of 100"), the alert card when passwords showed up in data breaches (or logins sit on breached websites; See details), the report as big-number tiles Compromised, Reused, Weak, Secure (tone only in the icon chip; a tap opens the list), the opt-in breach check (Check leaked passwords, explaining k-anonymity; status "No data breaches found" or a count; results in memory only, re-checked when an item's revision changes), Fix these first, What costs points, and category rows with count pills: Easy to guess, Found in data breaches, Used more than once, Without two-factor code, Unsecured connection (glossary terms) and the further checks including Breached website. Each opens a list with fixes: Change password and Add 2FA code open the editor, Switch to https:// rewrites the address. Missing two-factor codes are advice and do not count against "secure".
Tools tab: the generator (password, words, PIN) with the value on a large card, strength, Generate another and Copy, plus the trash.
Profile tab: large title, account card (Marina gradient avatar with a Solar initial, email, server) with Sign in on another device, the alert card and the report tiles (both open Security), then Security (biometric unlock, screen lock as fallback, re-prompt interval, auto-lock, autofill status, lock now); Account (devices, activity, master password, Emergency Kit, recovery code, two-factor sign-in, own security code); Family & Team; Vault (trash, generator); Appearance (System/Light/Dark, website icons, Material You); Language; Help; About Passbox; sign out. Choices open dialogs with radio buttons.
Sign in on another device (see architecture/recovery.md): shows the device code and a QR code
passbox-link:v1?server=<url>&code=<code>, polls every 2 s, asks "Was that you?" with the new device's name and IP, then approves. The master password is never transferred; the link is cancelled when the screen closes or expires.Two-factor sign-in: status; setup after the master password (
reauthenticate) with a QR code and setup key for the authenticator app and the first code; recovery codes shown once; new recovery codes; turning it off with a code. Security keys are listed but managed in the web app.Recovery code: status, create (shown once) or remove, each after the master password.
Screen lock as fallback (opt-in, Android 11+): the biometric Keystore key is created with
AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIALand the prompt allowsDEVICE_CREDENTIAL; switching it sets biometric unlock up again with a new key. The setting says that anyone who knows the screen lock can then open the vault.Accessibility: touch targets ≥ 48 dp, text scales with the system font size (
sp, layouts reflow), headings and states (expanded/collapsed, selected tabs) for TalkBack, custom actions on list rows, the alert card and form errors are live regions, the decorative illustration and scan overlay are hidden from TalkBack, reduced motion stops shimmer, scan line and springs, and token colors keep WCAG 2.2 AA in both themes.Dates follow the app language, not the JVM default (
DateText): "valid until 31 October 2026" in team invitations, the Emergency Kit and password history.
User experience
- Onboarding: an intro on the backdrop gradient (the brand illustration, "Your passwords. Your server. Your rules.", Get started creates an account, Sign in), then sign-in or sign-up in a bezelled panel; unlock uses the same frame (account, master password, Unlock, Unlock with fingerprint or face). The Secret Key is checked locally before the server is asked ("The Secret Key is too short: 26 characters follow “P1”, 6 are missing."). "Forgot your master password?" opens an honest dialog (nobody can reset it; try variants; change it on another unlocked device; ask your organization; otherwise a new account). New master passwords need 12 characters and
acceptableMasterPassword; the strength meter shows the localized warning and suggestions. - Emergency Kit: after sign-up, "Open my vault" needs the last four characters of the Secret Key. "Save as PDF" (Storage Access Framework) and "Print or share PDF" (share sheet via
FileProvider; the file lives in the private cache only while the screen is open) create an A4 PDF withPdfDocument: sign-in address, email and Secret Key without truncation, the date, a line for the master password, the three steps for a new device and a helper line. AkitPendingflag keeps a banner in the vault and Settings until the kit was confirmed. Settings › Show Emergency Kit asks for the master password again;PassboxClient.revealSecretKeychecks it locally. - Unlock: "Wrong master password" (never the combined email/password/Secret Key message), "Use a different account" behind a divider and a confirmation that mentions the Secret Key; every sign-out says that the Secret Key is removed and offers to show the kit first.
- Biometrics: offered once right after the first unlock with the master password. The master password is required again after every device restart and every 14 days (Settings: 14 days, 30 days, only after a restart), see
MasterPasswordReprompt. After a master password change the biometric key is renewed with a new prompt. - Trash: "Move to trash" with a snackbar "“Sparkasse” moved to the trash · Undo"; the trash (vault toolbar and Settings) shows the days left, restores, deletes permanently and empties with a confirmation.
purgeTrash(false)runs after every online unlock. Trashed items never reach autofill (items()excludes them). - Item detail: tap to copy (see above), revealed secrets hide again after 30 seconds, "Show large" (numbered characters, digits in
accentText(deep Sky in light, Solar in dark) and symbols orange, screen kept on, survives rotation), "Previous passwords" with restore, "Linked apps" with remove, "Technical details" (IDs, revision, algorithms) collapsed by default. Secure notes show only their line count in lists. - Two-factor codes: the field is called "Two-factor code (2FA)". "Scan QR code" opens a CameraX preview in a large rounded viewfinder with corner brackets, the glowing scan line (still with reduced motion) and a torch button, after a short explanation of the camera permission (with a link to the system settings after a permanent denial); frames are decoded on the device with ZXing core (no Google Play services) and only
otpauth://codes are accepted. Before saving,totpInputProblemexplains a single six-digit code or an invalid key. - Generator: password, words (passphrase with separator, capitals and an optional number) and PIN.
- Security check: "5 of 7 logins are secure" instead of a score, amber for things to improve, and an honest description of what it does not check.
- Breached websites: the "Breached website" check lists logins whose site had a breach published after the password was set, with date and exposed data, the facts of each breach and Have I Been Pwned as the source; the count shows on the Security tab.
BreachCatalogJob(JobScheduler, no extra library, every 6 hours with a network) downloads a newer catalogue and posts a notification (channel "Data breaches"; Android 13+ asks forPOST_NOTIFICATIONSfrom the screen). Limits: Android defers jobs (Doze, App Standby buckets); with a locked vault only the catalogue is downloaded and the breach shows at the next unlock. - Settings: Devices (this device badge, last active, sign out one or all others), Activity (last 50 events; footnote that wrong master passwords are detected on the device and may not appear), Change master password, auto-lock "when the app is closed or hidden" / 1 / 5 / 15 / 30 / 60 minutes (default 5, as on iOS), language and "Technical details".
- Help mode:
FLAG_SECUREstays on every screen. Settings › Help explains why screenshots and screen sharing don't work and lists non-secret facts a helper can ask for (app and Android version, device, language, server, biometrics, autofill, auto-lock), with Copy and Share. - Autofill card: a one-time card in the vault, "Let Passbox fill in passwords", with numbered steps, a button to
Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICEand an "Autofill is on ✓" state once Passbox is the autofill service. - Decryption failures: a banner "2 items could not be decrypted" with the technical details; nothing is deleted.
- Errors: every Rust error maps to a localized message with a next step.
rate_limitedshows the wait in minutes: the client putsdetails.retryAfterSecondsinto the message (too many attempts; try again in N seconds), whichrateLimitMinutesparses. - Touch targets are at least 48 dp, buttons grow with the font size (
heightIn), and screen-reader labels are localized.
Families, teams and sharing
All verification and key handling is in the Rust core (architecture/sharing.md); the app shows what the core verified. SharingRepository (implemented by PassboxVaultRepository, same client and mutex) publishes a SharingSnapshot (vaults, organizations, shared vaults, warnings, own account ID) after every change and sync; the pure-Kotlin model is model/Sharing.kt, the screens and SharingViewModel are in ui/sharing.
- Vault list: below the category chips, "All vaults ▾" picks one vault; the menu lists personal vaults, then each family and team (families first, then by name) with its icon (
FamilyRestroom,Groups) and this account's role. Shared items carry a badge with where they live ("Doe family" for the family vault, "Customers · Sales" otherwise). New items go to the shown vault if it is writable, otherwise to "Private"; the editor offers a vault choice among writable vaults and says who will see the item. - Item detail: Lives in and Who can see it ("You, Anna and 2 others"; people by email, groups by name), which opens the vault's access list. View-only items (
view, an unverified member list, or no key yet) hide favorite, edit, trash and restore and explain why;VaultViewModelrefuses those changes as well, and the server's 403 messages are translated. - Family & Team (Settings): organizations, Create family (with the family vault's name), Create team, Accept invitation, Your security code. An organization shows members with role and status ("Waiting for confirmation" with a Confirm button for admins, "Paused", a warning icon for a changed key), pending invitations with Revoke (confirmation), shared vaults with the own role, New shared vault (admins), Leave family/Leave team with the consequences (owners cannot leave), and the organization code under technical details.
- Inviting: email and role as radio options with explanations (only owners make admins). The result offers Send link (share sheet,
ACTION_SENDtext with subject) and Copy link, and says that the server mails it when it can. - Confirming: the member's key is looked up in the key directory and the security code shown in groups of five digits; comparing is optional (Codes match – confirm, Confirm without comparing, Codes don’t match). Members: role, security code, Remove from organization with the consequence text. Vault access (managers): add a confirmed member with a role, remove access (rotates the key), Renew key now when
rotationPendingis set. - Accepting an invitation:
MainActivityhandlespassbox://invite?link=<https link>(andpassbox://invite/<token>#org=…) and offers itself in the "Open with" chooser for https links whose path contains/invite/on any host (not auto-verified, because self-hosted servers have no fixed domain; browsers stay the default). The link is kept inAppContainer.pendingInvitationuntil the vault is unlocked. Pasting works too (the parser finds the link in a pasted mail). Flow: preview (kind, invited email, role and its meaning, expiry) → Join family/Join team → "Waiting for confirmation" with the own security code. Links for another server or email and links without#org=are explained. - Warnings: a banner above the items ("1 security warning for sharing") opens a screen with a plain explanation per warning kind, the technical text, and for a changed key Compare and trust new code, which shows the code to compare before
acceptKeyChange. - Security code: Settings › Your security code (copy, TalkBack reads it group by group) and Check with the server (
verifyOwnKey). - Autofill includes shared logins (never trashed ones) and adds the vault to the dataset subtitle ("anna@example.com · Doe family") and to the picker.
Two-factor sign-in
Sign-in (OnboardingViewModel) and unlock (UnlockViewModel, also in the autofill unlock screen) catch PassboxException.TwoFactorRequired and show TwoFactorForm (ui/unlock/TwoFactor.kt): a code from the authenticator app or a recovery code, and "Remember this device for 30 days". TwoFactorController completes it through VaultRepository.completeTwoFactor. Security keys work in the web app only (they are bound to its domain); enrollment is only possible in the web app and Settings says so. A master password change that needs a fresh login explains to unlock with the code first (a login in the last 10 minutes is enough). Tests: ViewModelTest.unlockAsksForTheSecondFactor and FfiTwoFactorTest against a real server.
Autofill
Enable it in Settings → Autofill → "Set up autofill" (system setting "Autofill service"). In Chrome, also choose "Autofill using another service" in Chrome's settings; other browsers that support the Android autofill framework work directly.
Field detection (FormParser), in this order: autofillHints set by the app; HTML attributes of web forms (autocomplete such as username, current-password, new-password, one-time-code; type=password/email; hidden, search and button inputs are ignored); OTP keywords in IDs, names and hints; inputType (password and email variations); keywords in view IDs, HTML name/id, placeholders and hints in several languages. If a form has a password but no detected username, the text field right before the password is taken as the username. Only visible text fields count.
Target: the web domain reported in the structure is used only when the form is in a known browser (Matching.TRUSTED_BROWSERS). Any app can embed a WebView and report an arbitrary domain, so for all other apps the package name is the target.
Matching (Matching):
- Websites: a login matches if one of its URL fields has the same host or the same registrable domain (eTLD+1 from the public suffix list, via OkHttp's bundled copy) as the page.
accounts.google.commatchesmail.google.com;alice.github.iodoes not matchbob.github.io; IP addresses andlocalhostonly match exactly; only http(s) URLs count. - Apps: a login is suggested only if it is linked to the app's package name and one of the app's signing certificates (
AppSignaturesreads them withPackageManager.GET_SIGNING_CERTIFICATES, including earlier certificates after a key rotation; the launcher<queries>entry makes apps visible on Android 11+). A link with the right package name but another certificate is reported as a possible fake. Older links without a certificate (theandroid_appfield,androidapp://<package>URLs from imports) are shown as "not verified" and never filled without a confirmation. Package names are never guessed from website domains (com.github.androiddoes not matchgithub.com). - Nothing is filled into a non-matching app or site without an explicit choice: every response contains "Search vault", which opens a picker of all logins. Picking a login that is not a verified match shows a confirmation ("This login is saved for bank.example, but you are filling the app com.example.fake …", "Fill anyway"). For apps whose certificate could be read, "Remember for this app" links the app with its certificate. Item detail lists the linked apps with a remove action.
Linked apps in the item (model/LinkedApps.kt): the link lives inside the encrypted item, in extraJson, so the server sees nothing and every client keeps it (clients must preserve unknown extraJson keys):
{
"androidApps": [
{
"packageName": "com.example.bank",
"certSha256": ["14:6D:E9:83:C5:73:01:13:74:5D:A3:7F:6A:C4:B7:A3:80:1F:5A:CB:43:1A:2C:62:D9:61:FF:2A:B0:5C:1D:5A"]
}
]
}packageName is the Android application ID. certSha256 holds SHA-256 fingerprints of the signing certificates as 32 uppercase hex pairs separated by colons (the format of Digital Asset Links sha256_cert_fingerprints); readers should accept any case and separators. An entry with an empty list is a link by name only. Linking the same package again merges the certificates. Removing a link also removes it from the legacy android_app field and androidapp:// URLs.
Responses: matching logins (favorites first, up to 10) as dropdown entries, and as inline keyboard suggestions on Android 11+ (supportsInlineSuggestions). For OTP fields, each matching login with a one-time password offers its code; the code is computed when the user picks it, so it is never stale. Picking a login that has a one-time password fills it and copies the current code to the clipboard (marked sensitive, cleared after 90 seconds) for the next step of the sign-in. When the vault is locked, the response is a single "Unlock Passbox" entry that opens AutofillAuthActivity (master password or biometrics), which then returns the real response.
Saving: responses carry SaveInfo for password fields (username optional, FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE for web forms). On a save request, the credentials stay in memory in the app process and only an opaque ID travels in the intent; the activity asks to save a new login (with the website URL, or the app linked with its signing certificate) or to update the password of an existing login with the same username (the old password goes into "Previous passwords").
Security notes
- Device storage. The Rust client stores two values through the
SecureStorecallback:account(email, KDF parameters and the Secret Key) andcache(the vault as received from the server: already end-to-end encrypted).EncryptedFileStoreencrypts both with AES-256-GCM under a non-exportable Keystore key (hardware-backed where available), binds each value to its key name as associated data and writes atomically tonoBackupFilesDir. Backups and device transfer are disabled (allowBackup=false,data_extraction_rules.xml). Sign-out deletes the files and the Keystore key. - Biometric unlock (opt-in in Settings) stores the client's unlock key (
PassboxClient.unlockKey(), the derived master keys; never the master password) encrypted with a separate Keystore key that requires a strong biometric for every use (setUserAuthenticationRequired,AUTH_BIOMETRIC_STRONGwith timeout 0 on Android 11+, per-use authentication on 9 and 10) and is invalidated when biometrics are enrolled or removed (setInvalidatedByBiometricEnrollment).unlockWithKeyskips Argon2id, works offline and is fast enough for autofill. The stored key is deleted on sign-out, after a biometric lockout, when the Keystore key was invalidated, and when it no longer unlocks (master password changed on another device). - Master password re-prompt: biometric unlock is refused (the password is required) after a device restart (the last password unlock is older than
now - elapsedRealtime()) and after the configured interval (default 14 days). The last password unlock time is stored in plain preferences; it is not secret, and changing the clock can only cause an extra prompt. - Screens:
FLAG_SECUREon the main and autofill activities (every screen can show secrets), so they are excluded from screenshots, recordings and the recent-apps preview. Settings › Help is the replacement for screen sharing during remote help. - Auto-lock after the configured time without interaction (1, 5, 15, 30 or 60 minutes, default 5; older settings such as 4 hours keep working), also checked when the app returns from the background, or immediately when no Passbox screen is visible ("when the app is closed or hidden"). Locking clears decrypted items from Kotlin memory and the Rust session.
- Camera: requested only when the QR scanner opens; frames are analyzed in memory and never stored.
- Emergency Kit PDF: written only to a location the user picks, or to the private cache for the share sheet (deleted when the screen closes and on the next start). The Secret Key is revealed only after the master password was checked locally.
- Clipboard: copied secrets are marked sensitive (
ClipDescription.EXTRA_IS_SENSITIVEon Android 13+) and cleared after 90 seconds unless something else was copied. - Server address: https only; plain http is accepted only for
localhost,127.0.0.1,::1and the emulator host10.0.2.2. TLS certificates are checked by Android's system trust store (user-installed CAs are not trusted by default). - Logging: the app does not log; data classes holding secrets override
toString, and release builds stripLog.v/Log.d. R8 rules keep JNA, the UniFFI bindings and the JNI classes. - Kotlin strings (master password while typing) cannot be wiped from memory; the unlock key arrays are overwritten after use.
Item extras from the Rust client
See architecture/item-extras.md. PassboxVaultRepository calls them on Dispatchers.IO; VaultRepository keeps defaults (loops, null) so fakes and older bindings still work.
| Feature | FFI | Android |
|---|---|---|
| Created | Item.createdAt (0 = unknown) | metadata card; hidden when unknown |
| Item history | itemHistory(id) | versions with what changed afterwards and the saving device (updatedBy matched against sessions()); versions this device cannot decrypt say so and cannot be restored; restoring is updateItem(id, version.data) |
| Website icons | icon(domain) (server-side fetch, 7-day cache, works offline) | CachingWebsiteIcons decodes and keeps up to 200 in memory; Profile › Appearance › Show website icons (default on); monogram otherwise |
| Breach check | breachedItems() (whole vault), breachCheck(password) | opt-in in the security check; offline shows the network error |
| Security analysis | securityReport(options), dismissFinding(itemId, kind, dismissed) | score 0–100 with what costs points, Fix these first and More checks with a list per kind (Dismiss / Show again); see security-analysis.md |
| Last used, Last autofill | recordItemUse(id, COPY/AUTOFILL/OPEN), itemUsages() | recorded on copy, on open and by autofill (AutofillAuthActivity and suggestions picked directly, read from FillEventHistory; dataset IDs are item IDs); encrypted on the device only |
| Multi-select | trashItems(ids), setFavorite(ids, favorite) | failures are reported per item, undo restores the trashed ones |
| Per-item check | itemHealth(id) | the app derives the same problems from health() for the whole list at once (HealthReport) |
Limitations
- Not tested on a device or emulator yet. The APK is built and the FFI is tested end to end on the JVM, but the UI, the Keystore, BiometricPrompt and autofill have only been compiled, linted and unit-tested.
- Passkeys (Credential Manager provider,
CredentialProviderService) are not supported yet. Passkeys created by other clients are preserved:ItemData.extraJsonis passed back unchanged on every edit. - Attachments are not implemented. Sharing: groups (creating, members) and suspending members are only available in the web app; signing up from an invitation on an invite-only server needs the web app (the FFI's sign-up takes no invitation token yet).
- Autofill does not fill credit cards or identities yet, and has no Compatibility mode allowlist for browsers without autofill framework support.
- The QR scanner reads two-factor setup codes only. Scanning the Secret Key from an Emergency Kit or a "new device" QR code needs a shared format with the web kit first.
- No caps-lock hint on the unlock screen: Android does not report the caps-lock state of soft keyboards.
- Import is not on Android yet. Identities are shown and can get custom fields, but have no fields of their own in the editor.
- Material 3 Expressive components (button groups, loading indicator, floating toolbar) need a newer Compose BOM and are not used yet.
- The UI is verified with Paparazzi screenshots on the JVM, not yet on a device (TalkBack and font scaling were reviewed in code and screenshots only).