fix(security): app-lock lifecycle consistency (grace across Back, passphrase eviction limits) #119

Merged
JMR-dev merged 2 commits from fix-applock-lifecycle into main 2026-07-02 08:50:15 +00:00
JMR-dev commented 2026-07-02 08:29:31 +00:00 (Migrated from github.com)

Closes #101

Two lifecycle-consistency items from PR #45's review.

1. Grace period now survives leaving via Back (activity recreation)

AppLockGate (the pure lock/grace state machine) was a field of the Activity-scoped AppLockViewModel. On API 29/30, Back on the task root finishes the Activity and clears its ViewModelStore, destroying the ViewModel and its gate — so re-entry within the 30s grace re-armed a fresh LOCKED gate and demanded full re-auth, while leaving via Home and returning stayed unlocked. This contradicted the documented 30s grace rule the JVM tests pin.

Fix: hoist the gate to an application-scoped @Singleton (provided in SecurityModule) and inject it into AppLockViewModel instead of constructing it inline. The same instance is now reused across Activity recreation, so Back-then-return behaves identically to Home-then-return. A genuine cold start (process death) constructs a fresh gate that correctly starts LOCKED — so this does not weaken the cold-start rule, and (deliberately) does not persist backgroundedAt to disk, which would wrongly let a killed process skip re-auth (and would strand PassphraseSession, which is empty after process death).

2. PassphraseSession eviction — KDoc corrected + limitation documented

The KDoc promised the passphrase is "cleared on lock, timeout," but no path re-locked it on grace expiry, and true eviction is not achievable here: provideDatabase runs once per process and SQLCipher keeps the key in the already-open Room handle, so the secret lives for the process lifetime regardless. That DB-handle lifecycle is owned by #93 (provideDatabase) and #111 (DB re-architecture), so per the ticket I did not touch DatabaseModule/provideDatabase.

  • Corrected the PassphraseSession KDoc to state the process-lifetime limitation explicitly.
  • Added a code comment at the timeout re-lock in AppLockViewModel deferring full eviction to #93/#111.
  • Did not call session.lock() on timeout: it is the only separately-held copy, but it also drives EncryptedCacheGuard, so clearing it while the app is merely locked (not exited) would stall background sync/push even though the DB stays open — i.e. it is not a correct side-effect-free partial eviction in the current architecture. The ticket's conditional "if there is a cheap correct partial eviction" is therefore not satisfied; documented why.

Tests (JVM unit)

  • AppLockGateTest: grace survives a reused-instance recreation within the window; still expires beyond it; a fresh gate (process death / the pre-fix Activity-scoped bug) starts LOCKED.
  • AppLockViewModelTest (new): the gate is an injected dependency the ViewModel delegates to (onBackground records on the injected gate; onAuthError re-locks through it) — a regression guard against re-inlining the gate. Broader ViewModel coverage remains issue #100.

Verification

Fast gate green locally (JDK 21): :app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt and :app:compileDebugAndroidTestKotlin.

🤖 Generated with Claude Code

Closes #101 Two lifecycle-consistency items from PR #45's review. ## 1. Grace period now survives leaving via Back (activity recreation) `AppLockGate` (the pure lock/grace state machine) was a field of the Activity-scoped `AppLockViewModel`. On API 29/30, Back on the task root finishes the Activity and clears its `ViewModelStore`, destroying the ViewModel and its gate — so re-entry within the 30s grace re-armed a fresh `LOCKED` gate and demanded full re-auth, while leaving via Home and returning stayed unlocked. This contradicted the documented 30s grace rule the JVM tests pin. **Fix:** hoist the gate to an application-scoped `@Singleton` (provided in `SecurityModule`) and inject it into `AppLockViewModel` instead of constructing it inline. The same instance is now reused across Activity recreation, so Back-then-return behaves identically to Home-then-return. A genuine cold start (process death) constructs a fresh gate that correctly starts `LOCKED` — so this does **not** weaken the cold-start rule, and (deliberately) does not persist `backgroundedAt` to disk, which would wrongly let a killed process skip re-auth (and would strand `PassphraseSession`, which is empty after process death). ## 2. `PassphraseSession` eviction — KDoc corrected + limitation documented The KDoc promised the passphrase is "cleared on lock, timeout," but no path re-locked it on grace expiry, and true eviction is not achievable here: `provideDatabase` runs once per process and SQLCipher keeps the key in the already-open Room handle, so the secret lives for the process lifetime regardless. That DB-handle lifecycle is owned by **#93** (provideDatabase) and **#111** (DB re-architecture), so per the ticket I did not touch `DatabaseModule`/`provideDatabase`. - Corrected the `PassphraseSession` KDoc to state the process-lifetime limitation explicitly. - Added a code comment at the timeout re-lock in `AppLockViewModel` deferring full eviction to #93/#111. - **Did not** call `session.lock()` on timeout: it is the only separately-held copy, but it also drives `EncryptedCacheGuard`, so clearing it while the app is merely locked (not exited) would stall background sync/push even though the DB stays open — i.e. it is not a *correct* side-effect-free partial eviction in the current architecture. The ticket's conditional "if there is a cheap correct partial eviction" is therefore not satisfied; documented why. ## Tests (JVM unit) - `AppLockGateTest`: grace survives a reused-instance recreation within the window; still expires beyond it; a fresh gate (process death / the pre-fix Activity-scoped bug) starts `LOCKED`. - `AppLockViewModelTest` (new): the gate is an injected dependency the ViewModel delegates to (`onBackground` records on the injected gate; `onAuthError` re-locks through it) — a regression guard against re-inlining the gate. Broader ViewModel coverage remains issue #100. ## Verification Fast gate green locally (JDK 21): `:app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt` and `:app:compileDebugAndroidTestKotlin`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.