Setting a PIN can lock you out of the session you set it in #62
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#62
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?
What is true now.
app/src/main/kotlin/dev/privacyllc/period/feature/lock/LockSettingsViewModel.kt:74-90writes the PIN and then unlocks the session:lock.setPin(pin)at:78,controller.unlock()at:85. Those are two independent dispatches.hasPinis a DataStore flow (core/security/.../AppLockRepository.kt:81); if its emission reachesAppLockViewModel(app/.../lock/AppLockViewModel.kt:85-94) before line 85 runs, the state flips to Locked,AppLockGate(AppLockGate.kt:67-86) invokescontent()only in the Unlocked branch and therefore disposes the wholePeriodAppsubtree — includingrememberNavController()and every nav-scoped ViewModel.LockSettingsViewModelis cleared, itsviewModelScopeis cancelled, andcontroller.unlock()never runs.The user is thrown to the lock screen and must type the PIN she created seconds ago; unlocking lands her on Today, not back in Settings.
What it costs.
docs/history/DEVELOPMENT_LOG.md:352-355records this defect as found and fixed ('Setting a PIN locked you out of the session you set it in'). The fix closed the common ordering but not the race, because the fix itself lives in the scope the race destroys. It was found by driving the emulator, not by a test, and there is still no test.What to do. Unlock BEFORE writing.
AppLockController.unlock()isMutableStateFlow.value = true— synchronous, on the caller's thread, complete beforesetPinis entered;hasPinflips insidestore.edit, strictly later, and reaches the gate'scombineby dispatch. Sincecombineemits with the latest value of each input, the pair (hasPin = true, unlocked = false) becomes unobservable. IfsetPinfails there is no PIN, and Unlocked is the correct state anyway, so nothing needs undoing.Traps. Never call
controller.lock()on failure — during a PIN change that throws the user to the lock screen behind their old PIN. Any later 'tidy-up' that moves the unlock back after the write re-introduces this; the test and the KDoc are the guard. Note PBKDF2 at 210k iterations runs synchronously insidesetPinon the calling thread — that is pre-existing jank on this path, not this race; do not conflate them.Verify:
setting the first PIN never closes the gateinLockSettingsViewModelTest— collectcombine(hasPin, unlocked, ::lockStateOf)throughout and assertLockState.Lockednever appears; swapping the two lines back to the old order must redden exactly that test.