mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
Review fixes: guard stale auth callbacks; clear the setting on fail-open
- Codex P1: backgrounding mid-authentication re-locked the app, but a Face ID success that resolved just after could still clear the new lock, bypassing the re-lock. Each lock bumps a generation and unlock callbacks from a superseded cycle are ignored. - Codex P2: the deliberate fail-open (device passcode removed) unlocked but left privacy.appLockEnabled true, so the toggle kept claiming the lock was on and re-adding a passcode silently reactivated it. The fail-open path now turns the setting off, matching the promised "turns itself off" behavior. Stale-callback path pinned by a new test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c8dd2180dc
commit
cc23827792
@ -45,6 +45,10 @@ final class AppLockModel: ObservableObject {
|
||||
|
||||
private let isEnabledProvider: () -> Bool
|
||||
private let authenticate: (@escaping @MainActor (Bool) -> Void) -> Void
|
||||
/// Bumped on every lock, so a success callback from a prior unlock
|
||||
/// attempt (e.g. Face ID resolving just as the app backgrounded and
|
||||
/// re-locked) cannot clear the new lock.
|
||||
private var lockGeneration = 0
|
||||
|
||||
/// Providers are injectable so tests drive the lock without LAContext
|
||||
/// or the shared UserDefaults.
|
||||
@ -67,14 +71,20 @@ final class AppLockModel: ObservableObject {
|
||||
|
||||
func lockIfEnabled() {
|
||||
guard isEnabledProvider() else { return }
|
||||
lockGeneration &+= 1
|
||||
isAuthenticating = false
|
||||
isLocked = true
|
||||
}
|
||||
|
||||
func requestUnlock() {
|
||||
guard isLocked, !isAuthenticating else { return }
|
||||
isAuthenticating = true
|
||||
let generation = lockGeneration
|
||||
authenticate { [weak self] success in
|
||||
guard let self else { return }
|
||||
// Ignore a callback for a lock cycle that has already been
|
||||
// superseded (backgrounded and re-locked mid-authentication).
|
||||
guard generation == self.lockGeneration else { return }
|
||||
self.isAuthenticating = false
|
||||
if success {
|
||||
self.isLocked = false
|
||||
@ -89,7 +99,11 @@ final class AppLockModel: ObservableObject {
|
||||
// Fail OPEN, deliberately: removing the device passcode already
|
||||
// requires knowing it, so this state means the owner disabled
|
||||
// it — locking them out of their own chats would punish exactly
|
||||
// the wrong person. The settings copy states this rule.
|
||||
// the wrong person. Turn the setting itself off too, so the
|
||||
// toggle stops claiming the lock is on and re-adding a passcode
|
||||
// later doesn't silently reactivate it (the copy promises the
|
||||
// lock "turns itself off").
|
||||
AppLockSettings.setEnabled(false)
|
||||
Task { @MainActor in completion(true) }
|
||||
return
|
||||
}
|
||||
|
||||
@ -2420,6 +2420,23 @@ struct AppLockModelTests {
|
||||
#expect(model.isLocked)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func staleAuthCallbackCannotUnlockAfterRelock() {
|
||||
var pending: ((Bool) -> Void)?
|
||||
let model = AppLockModel(
|
||||
isEnabledProvider: { true },
|
||||
authenticate: { completion in pending = completion }
|
||||
)
|
||||
#expect(model.isLocked)
|
||||
|
||||
// Auth begins, then the app backgrounds and re-locks before Face ID
|
||||
// resolves; the late success must be ignored.
|
||||
model.requestUnlock()
|
||||
model.lockIfEnabled()
|
||||
pending?(true)
|
||||
#expect(model.isLocked)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func staysInertWhenDisabled() {
|
||||
let model = AppLockModel(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user