Skip to content

Commit 54a84b4

Browse files
committed
Harden Archive lock: foreground-only biometric suppress, no stale handlers
- Relock on true background even during Face ID suppress (suppress only while still foreground). - Refresh willRelock/didBecomeActive handlers on every bind; drop dual first-wins closures that could keep a previous account's context. - Dismiss waits for full archived peer ids instead of empty-set first pass. - Always clear biometric suppress on LocalAuth completion. Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
1 parent ecff5e7 commit 54a84b4

3 files changed

Lines changed: 56 additions & 31 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
## [Unreleased]
88

9+
## [v12.9.2-4013]
10+
11+
### Fixed
12+
- **Archive lock:** Face ID resign no longer skips a real Home-background relock; suppress only applies while still foreground. Dual willRelock/didBecomeActive callbacks collapsed to session handlers refreshed on every bind (no stale account after switch). Dismiss waits for full archived peer-id set instead of first applying empty (App Switcher leak). Biometric suppress counter always cleared on auth completion.
13+
914
## [v12.9.2-3950-pre]
1015

1116
### Changed

‎submodules/ChatListUI/Sources/ArchiveLockHelpers.swift‎

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -361,19 +361,22 @@ public func relockArchiveSessionIfLeavingArchivedSurface(
361361
public func bindArchiveLockSession(context: AccountContext) {
362362
ArchiveLockSession.shared.bindBackgroundRelock(
363363
applicationIsActive: context.sharedContext.applicationBindings.applicationIsActive,
364-
willRelock: { [weak context] in
365-
guard let context else {
366-
return
367-
}
368-
prepareArchivePrivacyOnResignActive(context: context)
369-
},
370-
didBecomeActive: { [weak context] in
371-
guard let context else {
372-
return
373-
}
374-
restoreArchivePrivacyOnBecomeActive(context: context)
375-
}
364+
applicationInForeground: context.sharedContext.applicationBindings.applicationInForeground
376365
)
366+
// Refresh every bind so account switches do not keep a stale primary's prepare/restore.
367+
// (bindBackgroundRelock itself is first-wins for the subscription only.)
368+
ArchiveLockSession.shared.willRelockHandler = { [weak context] in
369+
guard let context else {
370+
return
371+
}
372+
prepareArchivePrivacyOnResignActive(context: context)
373+
}
374+
ArchiveLockSession.shared.didBecomeActiveHandler = { [weak context] in
375+
guard let context else {
376+
return
377+
}
378+
restoreArchivePrivacyOnBecomeActive(context: context)
379+
}
377380
ArchiveLockSession.shared.bindPasswordProtection(accountId: context.account.id.int64, isPasswordConfigured: archivePasswordProtectionSignal(context: context))
378381
ArchiveLockSession.shared.bindLockedPeerIds(accountId: context.account.id.int64, lockedPeerIds: archiveLockedPeerIdsSignal(context: context))
379382
}
@@ -439,11 +442,20 @@ public func ensureArchiveUnlocked(
439442
// falls through to the password prompt — never a dead end.
440443
if settings.useBiometrics, LocalAuth.biometricAuthentication != nil {
441444
// Biometric UI resigns active; suppress Archive background-relock for that window
442-
// so Face ID does not clear reveal / dismiss the folder mid-unlock.
445+
// so Face ID does not clear reveal / dismiss the folder mid-unlock. Always end
446+
// suppress on next *or* completion so a cancelled auth cannot stick the counter.
443447
ArchiveLockSession.shared.beginSuppressBackgroundRelock()
448+
var endedSuppress = false
449+
let endSuppress = {
450+
guard !endedSuppress else {
451+
return
452+
}
453+
endedSuppress = true
454+
ArchiveLockSession.shared.endSuppressBackgroundRelock()
455+
}
444456
let _ = (LocalAuth.auth(reason: ArchiveLockLocalizedString.biometricReason)
445457
|> deliverOnMainQueue).start(next: { success, _ in
446-
ArchiveLockSession.shared.endSuppressBackgroundRelock()
458+
endSuppress()
447459
if success {
448460
// Same reset the password path does on success. Both outcomes mean the owner
449461
// proved who they are, and the counter throttles guessing, not the owner — but
@@ -455,6 +467,8 @@ public func ensureArchiveUnlocked(
455467
} else {
456468
showPasswordPrompt()
457469
}
470+
}, completed: {
471+
endSuppress()
458472
})
459473
} else {
460474
showPasswordPrompt()
@@ -903,12 +917,17 @@ public func dismissOpenArchiveControllers(from navigationController: UINavigatio
903917
apply(ArchiveLockSession.shared.currentLockedPeerIds())
904918
return
905919
}
906-
apply(ArchiveLockSession.shared.currentLockedPeerIds())
920+
// Do not apply an empty peer-id set first: that would pop the Archive folder but leave
921+
// archived chats / Peer Info / gallery on the stack until the async fetch lands (App
922+
// Switcher leak window). Wait for the full id set when we have a context; otherwise only
923+
// the folder match (empty ids still dismisses `.archive` chat lists) is available.
907924
if let context {
908925
let _ = (context.account.postbox.transaction { transaction -> Set<EnginePeer.Id> in
909926
return Set(transaction.chatListGetAllPeerIds(groupId: Namespaces.PeerGroup.archive))
910927
}
911928
|> deliverOnMainQueue).startStandalone(next: apply)
929+
} else {
930+
apply([])
912931
}
913932
}
914933

‎submodules/TelegramUIPreferences/Sources/ChatArchiveSettings.swift‎

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -508,12 +508,11 @@ public final class ArchiveLockSession {
508508

509509
/// Re-lock Archive when the app leaves the active state.
510510
///
511-
/// `willRelock` runs on the main queue *before* `relock()` so callers can cover Archive UI
512-
/// for the app-switcher snapshot while `isUnlocked` is still true. `didBecomeActive` runs
513-
/// when the app returns so that covering view can be removed if App Lock did not take over.
514-
/// Session-level `willRelockHandler` / `didBecomeActiveHandler` are also invoked at fire
515-
/// time so they can be registered after this binding has already claimed the slot.
516-
public func bindBackgroundRelock(applicationIsActive: Signal<Bool, NoError>, willRelock: (() -> Void)? = nil, didBecomeActive: (() -> Void)? = nil) {
511+
/// Handlers are looked up at fire time (`willRelockHandler` / `didBecomeActiveHandler`) so
512+
/// account switches can refresh them without rebinding the subscription. `applicationInForeground`
513+
/// distinguishes Face ID resign-active (still foreground — suppressible) from a true Home
514+
/// background (always relock, even mid-biometric).
515+
public func bindBackgroundRelock(applicationIsActive: Signal<Bool, NoError>, applicationInForeground: Signal<Bool, NoError>) {
517516
// Claim the "binding" slot atomically with a placeholder before subscribing, so two
518517
// concurrent callers can't both observe "not yet bound" and both subscribe — only the
519518
// caller that wins the claim installs a real disposable; the loser's subscription is
@@ -527,23 +526,25 @@ public final class ArchiveLockSession {
527526
if alreadyBound {
528527
return
529528
}
530-
let disposable = (applicationIsActive
531-
|> distinctUntilChanged
532-
|> deliverOnMainQueue).startStrict(next: { [weak self] isActive in
529+
let disposable = (combineLatest(applicationIsActive, applicationInForeground)
530+
|> distinctUntilChanged(isEqual: { lhs, rhs in
531+
return lhs.0 == rhs.0 && lhs.1 == rhs.1
532+
})
533+
|> deliverOnMainQueue).startStrict(next: { [weak self] isActive, inForeground in
534+
guard let self else {
535+
return
536+
}
533537
if isActive {
534-
didBecomeActive?()
535-
self?.didBecomeActiveHandler?()
538+
self.didBecomeActiveHandler?()
536539
} else {
537-
guard let self else {
538-
return
539-
}
540540
self.lock.lock()
541541
let suppressed = self.suppressBackgroundRelockCount > 0
542542
self.lock.unlock()
543-
if suppressed {
543+
// Face ID/Touch ID resigns active while still foreground — skip. A real
544+
// background (Home / app switcher settle) must always relock.
545+
if suppressed && inForeground {
544546
return
545547
}
546-
willRelock?()
547548
self.willRelockHandler?()
548549
self.relock()
549550
}

0 commit comments

Comments
 (0)