App lock: a PIN just for this app, a fingerprint or face, or either #63
Labels
No Label
P0
P1
P2
release-blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: null/Privacy-Period-Tracker#63
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Asked for by Kaspa: 'leave it an option to either have biometrics or a custom app pin. Let it be known that the pin is custom to the app.'
What is true now. Biometrics already ship (
androidx.biometric:1.1.0,app/build.gradle.kts:101;app/.../lock/BiometricUnlock.kt), but only as a shortcut layered on a MANDATORY PIN: the lock is on exactly when a PIN verifier exists (AppLockViewModel.kt:85-94) andbiometricAllowedisbiometricLockEnabled && hasPin(:97-101). The fingerprint toggle is rendered only inside the has-a-PIN branch (LockSettingsScreen.kt:136-163). There is no biometrics-only path. The biometric preference also lives inUserPreferences(core/datastore/.../UserPreferences.kt:48) — the storeresetToDefaults()wipes — while the PIN record lives in its ownapp_lockDataStore precisely to avoid that.What to do. A
LockMethod { NONE, PIN, BIOMETRIC, PIN_AND_BIOMETRIC }stored in theapp_lockstore, resolved against the verifier so a method that needs a PIN without one reads as off. A one-time migration (hasPin && flag → PIN_AND_BIOMETRIC; hasPin → PIN; else NONE) that can never produce BIOMETRIC. Settings offers the four choices with one-line explanations; every method change first authenticates with the current method, and any target that allows biometrics needs a successful scan in-flow before it is committed. The lock screen in biometric-only mode shows one button and no PIN field.The PIN copy must say what Kaspa asked for, in the consent screen and beside the option: this PIN is for this app only, separate from the phone's own PIN, pattern or password — knowing one does not open the other. The biometric copy must say who it lets in: anyone whose fingerprint or face is set up on this phone, now or later.
Traps. A migration that leaves someone locked out is a P0 —
recordMigratedMethodmust refuse BIOMETRIC at the API and write only inside a conditional DataStore transaction. In biometric-only mode an unavailable sensor must NOT auto-disable the lock (todayBiometricUnlock.kt:85-110persists the flag off on ERROR_NO_BIOMETRICS, which in biometric-only mode would let anyone who knows the phone PIN remove the fingerprints and walk in); stay locked and point at phone settings or the erase path.DEVICE_CREDENTIALmust never be requested —SECURITY.md:64-72forbids it andPromptInfo.build()throws for it on API 28/29. A submitted PIN must never unlock a BIOMETRIC-mode lock even if a stale verifier exists. The settings biometric prompt must setauthInProgressor the gate relocks and tears down Settings under the prompt.Verify:
LockMethodMigrationTestincludingthe migration never produces fingerprint-onlyacross all four legacy combinations (proved withscripts/prove-guard.sh),LockSettingsViewModelTesttransition tests, andLockSettingsCopyTestasserting the PIN option names both 'app' and 'phone'.Shipped across three commits:
9b2b332(storage and migration),750fdae(state machine and lock screen),f91ba6e(the choice on screen).LockMethod { NONE, PIN, BIOMETRIC, PIN_AND_BIOMETRIC } lives in the lock's own DataStore, not UserPreferences — resetToDefaults() there is edit { clear() }, so a fingerprint-only lock recorded beside the theme would be one settings reset from vanishing. resolve() reads a verifier with no recorded method as PIN (so an old install's gate is shut from the first frame), a PIN-requiring method with no verifier as NONE (a lock nobody can open is worse than none), and an unknown name falls back to the verifier.
The migration can never produce BIOMETRIC: recordMigratedMethod refuses it at the API, and both its conditions are evaluated inside the DataStore transaction so a migration racing an erase cannot resurrect a removed lock. LockMethodMigrationTest walks all four legacy combinations asserting it.
Three rules with tests: any change authenticates with the method in force; a fingerprint is never enabled without one succeeding on that phone; moving to fingerprint-only drops the verifier AND the Keystore key only after that scan. In fingerprint-only mode an unavailable sensor does not switch the lock off — that would make it removable by anyone who knows the phone's own PIN — the screen names re-enrolment or erase instead.
The copy says what Kaspa asked for: 'A PIN just for this app — a PIN you choose here, separate from your phone's. Nobody can reset it.' LockSettingsCopyTest asserts the PIN option names both 'app' and 'phone', the fingerprint option names 'anyone', 'this phone' and 'later', and the whole surface avoids the sensitive-word list and the words safe/secure/encrypted/protected.
Verify: ./gradlew :core:security:testDebugUnitTest :app:testDebugUnitTest — AppLockRepositoryTest 24, LockMethodMigrationTest 9, LockSettingsViewModelTest 11, LockSettingsCopyTest 5, LockCopyTest 3, LockSettingsBackTest 4, ForgotPinBackTest 1, AppLockControllerTest 6, all green. Proved with scripts/prove-guard.sh: removing the migration's BIOMETRIC refusal, and making a failed scan commit, each redden exactly one test. Confirmed on the PeriodQA emulator — the four options render, and both biometric rows are disabled with 'this phone has no fingerprint or face set up', which is true of that image.