Skip to content

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 settings table; salt regenerated on every PIN change so identical PINs never share a digest; verified with constantTimeEquals.
  • 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 = 5 failures, lock for LOCKOUT_MS = 30 s; the lock screen shows a countdown.
  • Lifecycle relockonStop relocks (registered on ProcessLifecycleOwner).
  • Biometric — optional, WEAK/STRONG selectable, optional device-credential fallback; availability probed via BiometricManager; prompt hosted in MainActivity, requested once per lock session.
  • Testability — depends on the SettingsStore seam and injectable LockClock, 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 MainActivity above 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.