ADR-0012: App lock
- Status: Accepted
- Date: 2026-08-11
- Deciders: maintainers
- Related: security.md
Context
The app holds sensitive financial history. Users want a PIN/biometric gate against casual access, with protection against brute force and relock when the app leaves the foreground.
Decision
An AppLockManager (singleton, DefaultLifecycleObserver) gates the UI:
- PIN storage — salted SHA-256 digest in the encrypted
settingstable; salt regenerated on every PIN change so identical PINs never share a digest; verified withconstantTimeEquals. - Session unlock — per-process flag with an idle timeout
(
KEY_LOCK_TIMEOUT_MINUTES, default 5 min; 0 = never). - Brute-force lockout — after
MAX_FAILED_ATTEMPTS = 5failures, lock forLOCKOUT_MS = 30 s; the lock screen shows a countdown. - Lifecycle relock —
onStoprelocks (registered onProcessLifecycleOwner). - Biometric — optional, WEAK/STRONG selectable, optional device-credential
fallback; availability probed via
BiometricManager; prompt hosted inMainActivity, requested once per lock session. - Testability — depends on the
SettingsStoreseam and injectableLockClock, so security logic is unit-tested pure JVM.
Implementation note (2026-09, issue #95): the idle deadline is
re-evaluated while the app stays in the foreground by a 15 s ticker in
AppLockViewModel (viewModelScope, skipped while locked); navigation
destination changes call AppLockManager.recordActivity() so the timeout
measures inactivity, not wall time since unlock. The ticker lives in the
ViewModel because common/ stays Android-free and viewModelScope is
cancelled with the composition automatically.
The PIN/settings live in the already-encrypted DB (settings table), not in SharedPreferences.
Consequences
- Brute force is throttled; identical PINs across resets do not share digests; leaving the app relocks.
- Lock screen overlays the whole UI (rendered in
MainActivityabove the NavHost) and appears only after onboarding. - Settings expose PIN length (4/6/8), auto-lock interval, biometric strength, and credential fallback.
Alternatives considered
- Keystore-backed auth for PIN — rejected: needs a confirmed-credential round trip; salted hash in the encrypted table is sufficient for a per-app gate and is unit-testable.
- No lock — rejected: sensitive financial data visible on an unlocked phone.