-
Notifications
You must be signed in to change notification settings - Fork 2k
Cache keychain passwords to prevent duplicate prompts #5982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -62,6 +62,41 @@ private struct BrowserConfig { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - Shared Keychain Cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Shared cache for browser keychain passwords so we only prompt once per session. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Used by both GmailReaderService and CalendarReaderService. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final class BrowserKeychainCache: @unchecked Sendable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let shared = BrowserKeychainCache() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var cache: [String: String] = [:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let lock = NSLock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func get(_ service: String) -> String? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { lock.unlock() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cache[service] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func set(_ service: String, password: String) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { lock.unlock() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache[service] = password | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Returns true if we already tried and failed for this service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func hasAttempted(_ service: String) -> Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { lock.unlock() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cache.keys.contains(service) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func markFailed(_ service: String) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { lock.unlock() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache[service] = "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+93
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This means: if a user logs into a browser after the app has already attempted (and failed) a keychain lookup, the credential will never be picked up without a full app restart. While the PR description intentionally scopes this to "one session," it may be surprising in practice — e.g., during a first-run onboarding flow where the browser is opened after the desktop app. Consider calling |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+69
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - GmailReaderService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actor GmailReaderService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -525,6 +560,14 @@ actor GmailReaderService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - Keychain | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func getKeychainPassword(service: String) -> String? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check shared cache first to avoid duplicate keychain prompts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let cached = BrowserKeychainCache.shared.get(service) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cached.isEmpty ? nil : cached | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if BrowserKeychainCache.shared.hasAttempted(service) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+564
to
+569
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The
The result is that the block Fix: remove the redundant check, or change
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let query: [String: Any] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kSecClass as String: kSecClassGenericPassword, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kSecAttrService as String: service, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -536,11 +579,15 @@ actor GmailReaderService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard status == errSecSuccess, let data = result as? Data, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let password = String(data: data, encoding: .utf8) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BrowserKeychainCache.shared.markFailed(service) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if status != errSecItemNotFound { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log("GmailReaderService: Keychain lookup for '\(service)' failed with status \(status)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !password.isEmpty { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BrowserKeychainCache.shared.set(service, password: password) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return password.isEmpty ? nil : password | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasAttemptedcheck as inGmailReaderServiceThe same unreachable logic described in
GmailReaderService.swiftis duplicated here. Sincegetalready handles the""sentinel,hasAttemptedcan never returntrueat this point, and the block is dead code.