Imported from Busnes-app/KyAuth-android (
AGENTS.md). Install upstream withnpx skills add Busnes-app/KyAuth-android. Copyright stays with the author.
KyAuth Android Client
KyAuth is the native Android authenticator for the KySecurity suite.
Purpose
KyAuth pairs an Android device with KySignOn. It stores TOTP entries in an encrypted local KDBX v4 vault. It also provides a local biometric and optional PIN lock.
Current product contract
- Pairing accepts a short-lived KySignOn QR payload or manual server details.
- Release builds require HTTPS. Debug builds permit loopback HTTP only.
- The optional registration URL must use the same origin as the pairing server.
- The app generates a hardware-backed P-256 device signing key.
- TOTP entries use KeePass
TimeOtp-*fields intotp_vault.kdbx, along with standard KeePass title, URL, and notes. - The TOTP vault uses an app-private file and an independent random vault key.
- Both vault keys are wrapped by
VaultKek, an authentication-bound Keystore RSA-OAEP key (setUserAuthenticationRequired(true), per-use). Wrapping needs no prompt; unwrapping requires aBiometricPrompt.CryptoObject. A device without a secure lock screen cannot use KyAuth. - One authentication yields one unwrap, so both keys share a single wrapped blob.
- The app locks when it moves to the background. It clears in-memory TOTP data and its copied code, and zeroes the vault key arrays.
- The PIN is an optional second local factor. Failed PIN attempts use delays of 0, 5, 30, and 300 seconds. The fifth failure wipes local data.
- Release builds disable screenshots and Android backup.
- Push MFA receives KySignOn FCM data-message challenges, posts a local notification, and opens the Push MFA tab for approve/deny. A response is only ever sent to the paired server; a
serverUrlin the push payload is ignored. Digits must be two-digit, decoys are capped at 3, and expiry is clamped to 10 minutes. - An MFA response must carry an explicit decision. A 2xx with no
approved/successfield is a protocol error, not an approval. - The KyPasswords key envelope must declare
kdf: argon2id, and derivation uses the envelope's ownmemoryKiB/iterations/parallelism(Argon2id v1.3). Any other value, including a missing field, is refused rather than guessed at: the superseded PBKDF2 shape marked itself by omittingkdf, and with no KyPasswords deployment holding one, no envelope of that shape exists to read. Costs are server-supplied, so they are range-checked before anything is allocated (256 MiB ceiling) and rejected, not clamped. KyAuth writes the OWASP baseline (64 MiB, t=3, p=1). - KDBX entry fields use the KeePass wire names from kotpass's
BasicField.key, not the enum constantname. The two differ only for the URL field (URLvsUrl); KyAuth used the constant, so its URLs were invisible to KeePassXC, KeePassDX and the KyPasswords web client, and theirs to KyAuth. Proven bykypasswords-web-vault.kdbx, a fixture written by kdbxweb. - Passwords and Passkeys use
passwords_vault.kdbx. The app can generate an independent random local vault key for device-only storage. Pairing with an empty KyPasswords account uploads that local vault with a client-created password envelope; pairing never replaces an existing server vault that uses another key. - A passkey whose RP ID is the paired KySignOn server's host is the exception: its private key is
generated in AndroidKeyStore (StrongBox where available, TEE otherwise), is non-exportable, and
never enters a KDBX vault or any synced artifact. Only its metadata is stored, in
SignOnPasskeyStore. The assertion path never callsAppLockManager.useVaultKeys, so KySignOn MFA keeps working while the password vault is locked, compromised, or in recovery. The Credential Provider therefore offers this one entry while KyAuth is locked, alongside the unlock action. Losing the device means falling back to KySignOn recovery codes or an admin MFA reset. - Passkeys use native ES256 / P-256 WebAuthn cryptography with COSE public key encoding and ECDSA assertion signing.
- KyAuth acts as an Android 14+ (API 34+) system Credential Provider for both Passkeys and Passwords via
KyAuthCredentialProviderService(withCredentialAuthActivity) and an Android 12+ (API 31+) system Autofill Service viaKyAuthAutofillService. - While locked, neither provider touches vault material. Autofill returns a
FillResponsewith an authenticationIntentSender(AutofillUnlockActivity). The Credential Provider returns an authenticationAction(CredentialUnlockActivity) for anything vault-backed, and, when a KySignOn passkey is enrolled, a real credential entry for it directly alongside that action — the passkey entry needs no vault key, which is why it can be offered while locked. The vault-backed paths unwrap the keys for one operation viaAppLockManager.useVaultKeysand erase them again, so a background request never unlocks the app. - Passkey RP IDs are validated by
RpId: syntactically valid, not a public suffix, and for browser callers equal to or a registrable parent of the caller's web origin. Native-app callers are bound to the RP byDigitalAssetLinks, which fetcheshttps://<rpId>/.well-known/assetlinks.jsonand matches the caller's signing certificate. It fails closed: no statement, or an offline device with a cold cache, means no passkeys are offered to a native caller. - A caller-supplied
clientDataHashis honoured only from a caller that set a privileged web origin (ClientData.privilegedClientDataHash). Only a holder ofCREDENTIAL_MANAGER_SET_ORIGINcan set that origin, so an ordinary app cannot choose the bytes KyAuth signs. For every other caller theCollectedClientDatais built here, with theandroid:apk-key-hash:origin. - Autofill believes a request's
webDomainonly from a known browser installed as a system app or by Google Play; any other caller is matched on its own package.setWebDomainis public API, so an unfiltered domain from an arbitrary app would hand one site's credential to another. - Password fill matches an entry's domain or its subdomains, never a parent or sibling, and never across a public suffix. Passkey matching is exact on RP ID.
- Incremental password/passkey edits use
KdbxPasswordVault.update; delete uses its serializeddeleteoperation. Both mutate the decoded KDBX by UUID, retaining groups, unknown fields, attachments, history and metadata.saveEntriesonly creates a new file. Existing empty or unreadable files fail closed. Live reads exclude the metadata-identified recycle bin and its descendants. Disabled recycling requires explicit permanent-delete confirmation. User edits honor the file's history item/content-size budgets; signCount-only updates add no history. Password vault reads and mutations run on worker threads; only their results reach the UI. KyPasswordVaultSyncserializes sync sessions and uploads immutable encrypted snapshots. Downloads are decoded before installation under the local vault monitor. A remote replacement requires an unchanged local file and a known clean sync fingerprint. Unknown or dirty state, concurrent local writes and HTTP 409 preserve encrypted versions inpassword-vault-conflictsand surface a conflict; they never merge UI projections or automatically overwrite either side. Byte-identical local/remote files establish a missing baseline on upgrade. Explicit resolution chooses the whole device or server vault, guarded by If-Match and local-change detection. Clean successful syncs and unpairing remove conflict copies; startup/sync sweeps interrupted snapshots. Unpair clears the account, key and files in one vault transaction, so a new local vault cannot overtake teardown. A validated master-password key is adopted before sync, so network errors leave local access and conflict resolution available. Passwords can export those files; revealing their opening key uses the existing authenticated offline-key flow. Local wipe removes the conflict files along with all app-private files.- The Passwords tab supports pairing with KyPasswords, syncing vaults, local add, generate, list, reveal, copy, and delete actions with distinct Passkey badging. Reveal and copy require a biometric or device-authentication prompt.
- The Passwords Recycle Bin is a metadata-only deleted-entry view, including descendants and untitled/non-password records. Restore moves the intact original entry to the live root; it preserves the UUID and all contents. There is no purge action. App lock clears and dismisses open dialogs, including unsaved forms and revealed secrets.
- Reused passwords compares exact nonempty strings across all live KDBX records, including whitespace-only passwords and untitled records. Its results contain metadata and counts only; passwords are never logged or sent. Open recovery/reuse views refresh after vault reloads.
- Foreground idle locking defaults to five minutes, configurable to 1/5/15/30/60 minutes in Settings. It uses elapsed realtime, includes dialog activity, and checks expiry before accepting new input. Background locking remains immediate. Lock generations reject stale asynchronous unlock/reveal results; only the UI thread installs loaded entry lists.
- Copied passwords are marked sensitive and clear after 30 seconds or when KyAuth locks.
UI contract
- Use the
KyAuthname in user-visible text. - Keep the KyAuth Systems stamp shield and KyAuth wordmark in the header and lock screen.
- Use the five-part bottom pill: TOTP Vault, Push MFA, lock shield, Passwords, Settings.
- The TOTP Vault screen provides a + icon to scan QR or add accounts manually with optional Website and Notes fields.
- Use the 15 suite themes from
ThemeManager. The default is Patina Ky. - Use rounded, flat buttons. Do not add elevation shadows to custom controls.
Project layout
app/src/main/java/org/kysecurity/authenticator/MainActivity.kt: app UI and workflows.pairing/: QR parsing, endpoint validation, pairing network client, device key, and encrypted pairing store.mfa/: push challenge model, FCM receive service, signed payload, and response client.security/: lock state, PIN policy,VaultKekauthentication-bound key wrapping,VaultUnlockPrompt, atomic file writes, and local wipe.totp/: TOTP parsing, generation, and KDBX persistence.passwords/: password/passkey entry models, domain matcher, password generator, autofill service, and KDBX persistence.passkeys/: FIDO2 WebAuthn crypto engine,ClientData(CollectedClientData),RpIdvalidation,SignOnPasskeyrouting plus its hardware key and metadata store, CredentialProviderService, entry builder, slice builder, unlock activity, and auth activity.ThemeManager.kt: the shared 15-theme palette and local theme preference.UiComponents.kt: reusable programmatic view styling and controls.AboutDialog.kt: MIT About dialog.
Work guidance
- Use the smallest correct change.
- Reuse native Android APIs and existing project code before adding dependencies.
- Fix shared root causes, not one call site.
- Keep security checks fail-closed.
- Add a focused test for non-trivial logic.
- Update this file when a durable product contract, workflow, or file boundary changes.
Verification
Run unit tests, lint, the debug build, and compile device tests:
./gradlew test lintDebug assembleDebug compileDebugAndroidTestSources
npm ci --prefix tools --ignore-scripts
pip install argon2-cffi==25.1.0
node tools/vault_preservation.js app/build/interop
KdbxPreservationTest writes the Android round-trip outputs consumed by the Node verifier.
Regenerate the fake-secret rich fixtures with node tools/vault_preservation.js generate.
Outstanding security work
Recorded so it is not mistaken for done:
- Non-Play browser builds.
TrustedBrowsersaccepts known browser packages only when installed by Google Play or as system apps. F-Droid and direct-download builds fail closed until explicit signing-certificate pins are maintained for them. - Vault size ceilings.
KyPasswordClientandKdbxPasswordVaultcap a synced vault at 25 MB and a JSON body at 1 MB. Large legitimate vaults would need these raised. - Push MFA payload binding.
MfaMessage.formatPayloadstill signs onlyprefix|challengeId|verb|digits. Binding server origin, account, purpose and expiry needs a matching KySignOn server change. - Non-KySignOn passkey private keys are exportable. Deliberate: they live in the KDBX vault so they sync and restore, as other password managers do. Protection comes from the authentication-bound vault key. The KySignOn login passkey is the exception and is hardware-resident; see the product contract above.
- KySignOn passkey attestation. Even when the key is hardware-backed,
fmtis stillnone, so the server has only the client's word for it.setAttestationChallengeplus a verifier inkysignon-serverwould make it evidence. - KySignOn passkey hardware backing is unverified.
SignOnPasskeyKey.generateaccepts a key only whenKeyInfo.securityLevelisTRUSTED_ENVIRONMENT,STRONGBOXorUNKNOWN_SECURE, so bothSOFTWAREandUNKNOWN("the platform could not tell") are refused; the fail-closed path is covered by a passing instrumented test. The positive path is not: every available Android emulator ships the software KeyMint reference implementation, sogeneratereturning a hardware-backed key has never been observed succeeding. Four instrumented tests inSignOnPasskeyKeyTestare gated behind a JUnit assumption and SKIP rather than pass. Running them on a physical device is what closes this; until then, do not claim the key is hardware-resident. - Credential picker accumulation is unverified. While locked, the provider returns the KySignOn
entry alongside the unlock action, and
CredentialUnlockActivitydeliberately passessignOnPasskey = nullso the entry is not duplicated after unlocking. That is correct only if the framework ADDS an authentication action's entries to those already shown rather than replacing them. This was decided by reading AOSP, not by observation. Verify on a device: with KyAuth locked, trigger a KySignOn sign-in, tap "Unlock KyAuth", and confirm the KySignOn passkey is still offered. If it disappears, pass the record through inCredentialUnlockActivityinstead of null. - Device verification. Emulator tests cover secure-lock-backed
VaultKekcreation and backup flags. Full biometric prompts,useVaultKeys, provider unlock flows, and the per-useBiometricPromptfor the KySignOn passkey (enrolment and assertion, viaVaultUnlockPrompt.showForSignature) still require manual device verification; none of these are currently automated here. - Deprecated platform APIs.
Slice,EncryptedSharedPreferences/MasterKey, and theDataset/FillResponsebuilders are deprecated. Moving toandroidx.credentialswould remove most of the Slice usage.
Child DOX Index
No child AGENTS.md files exist.
Product icon
App/launcher assets use the Busnes.app-site Systems stamp family. Regenerate platform sizes from the matching master in ../Busnes.app-site; preserve resource names and adaptive foreground safe margins. This asset update does not change native theme defaults.
