-
-
Notifications
You must be signed in to change notification settings - Fork 237
Prevent duplicate iOS biometric prompts & silent reads #601
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 |
|---|---|---|
|
|
@@ -157,7 +157,11 @@ public final class HybridSensitiveInfo: HybridSensitiveInfoSpec { | |
| query[kSecReturnData as String] = kCFBooleanTrue | ||
| } | ||
|
|
||
| guard let raw = try copyMatching(query: query, prompt: request.authenticationPrompt) as? NSDictionary else { | ||
| guard let raw = try copyMatching( | ||
| query: query, | ||
| prompt: includeValue ? request.authenticationPrompt : nil, | ||
| allowAuthentication: includeValue | ||
| ) as? NSDictionary else { | ||
| return Variant_NullType_SensitiveInfoItem.first(NullType.null) | ||
| } | ||
|
|
||
|
|
@@ -196,17 +200,14 @@ public final class HybridSensitiveInfo: HybridSensitiveInfoSpec { | |
| public func hasItem(request: SensitiveInfoHasRequest) throws -> Promise<Bool> { | ||
| Promise.parallel(workQueue) { [self] in | ||
| let service = normalizedService(request.service) | ||
| var query = makeBaseQuery( | ||
| let query = makeBaseQuery( | ||
| key: request.key, | ||
| service: service, | ||
| synchronizable: request.iosSynchronizable, | ||
| accessGroup: request.keychainGroup | ||
| ) | ||
| query[kSecMatchLimit as String] = kSecMatchLimitOne | ||
| query[kSecReturnAttributes as String] = kCFBooleanTrue | ||
|
|
||
| let result = try copyMatching(query: query, prompt: request.authenticationPrompt) | ||
| return result != nil | ||
| return try itemExists(query: query) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -232,7 +233,11 @@ public final class HybridSensitiveInfo: HybridSensitiveInfoSpec { | |
| query[kSecReturnData as String] = kCFBooleanTrue | ||
| } | ||
|
|
||
| let result = try copyMatching(query: query, prompt: request?.authenticationPrompt) | ||
| let result = try copyMatching( | ||
| query: query, | ||
| prompt: includeValues ? request?.authenticationPrompt : nil, | ||
| allowAuthentication: includeValues | ||
| ) | ||
| guard let array = result as? [NSDictionary] else { | ||
| return [] | ||
| } | ||
|
|
@@ -368,18 +373,31 @@ public final class HybridSensitiveInfo: HybridSensitiveInfoSpec { | |
| SecItemDelete(deleteQuery as CFDictionary) | ||
| } | ||
|
|
||
| private func copyMatching(query: [String: Any], prompt: AuthenticationPrompt?) throws -> AnyObject? { | ||
| private func copyMatching( | ||
| query: [String: Any], | ||
| prompt: AuthenticationPrompt?, | ||
| allowAuthentication: Bool = true | ||
| ) throws -> AnyObject? { | ||
| #if targetEnvironment(simulator) | ||
| try performSimulatorBiometricPromptIfNeeded(prompt: prompt) | ||
| if allowAuthentication { | ||
| try performSimulatorBiometricPromptIfNeeded(prompt: prompt) | ||
| } | ||
| #endif | ||
| var workingQuery = query | ||
| if !allowAuthentication { | ||
| workingQuery[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail | ||
| } else if let prompt { | ||
| workingQuery[kSecUseOperationPrompt as String] = prompt.title | ||
| workingQuery[kSecUseAuthenticationContext as String] = makeLAContext(prompt: prompt) | ||
|
Comment on lines
+387
to
+391
|
||
| } | ||
|
|
||
| var result: CFTypeRef? | ||
| var status = performCopyMatching(query as CFDictionary, result: &result) | ||
| var status = performCopyMatching(workingQuery as CFDictionary, result: &result) | ||
|
|
||
| if status == errSecInteractionNotAllowed || status == errSecAuthFailed { | ||
| var authQuery = query | ||
| authQuery[kSecUseOperationPrompt as String] = prompt?.title ?? "Authenticate to access sensitive data" | ||
| let context = makeLAContext(prompt: prompt) | ||
| authQuery[kSecUseAuthenticationContext as String] = context | ||
| if allowAuthentication && prompt == nil && (status == errSecInteractionNotAllowed || status == errSecAuthFailed) { | ||
| var authQuery = workingQuery | ||
| authQuery[kSecUseOperationPrompt as String] = "Authenticate to access sensitive data" | ||
| authQuery[kSecUseAuthenticationContext as String] = makeLAContext(prompt: nil) | ||
| status = performCopyMatching(authQuery as CFDictionary, result: &result) | ||
| } | ||
|
|
||
|
|
@@ -388,11 +406,35 @@ public final class HybridSensitiveInfo: HybridSensitiveInfoSpec { | |
| return result as AnyObject? | ||
| case errSecItemNotFound: | ||
| return nil | ||
| case errSecInteractionNotAllowed, errSecAuthFailed: | ||
| throw runtimeError(for: status, operation: "fetch") | ||
| default: | ||
| throw runtimeError(for: status, operation: "fetch") | ||
| } | ||
| } | ||
|
|
||
| private func itemExists(query: [String: Any]) throws -> Bool { | ||
| var existenceQuery = query | ||
| existenceQuery[kSecMatchLimit as String] = kSecMatchLimitOne | ||
| existenceQuery[kSecReturnData as String] = kCFBooleanFalse | ||
| existenceQuery[kSecReturnAttributes as String] = kCFBooleanFalse | ||
| existenceQuery[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail | ||
|
|
||
| var result: CFTypeRef? | ||
| let status = performCopyMatching(existenceQuery as CFDictionary, result: &result) | ||
|
|
||
| switch status { | ||
| case errSecSuccess: | ||
| return true | ||
| case errSecItemNotFound: | ||
| return false | ||
| case errSecInteractionNotAllowed, errSecAuthFailed: | ||
| return true | ||
| default: | ||
| throw runtimeError(for: status, operation: "existence check") | ||
| } | ||
| } | ||
|
|
||
| private func makeItem(from dictionary: NSDictionary, includeValue: Bool) throws -> SensitiveInfoItem { | ||
| guard | ||
| let key = dictionary[kSecAttrAccount as String] as? String, | ||
|
|
||
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.
This section suggests metadata-only enumeration has a “dedicated silent path” that still behaves like a normal listing. With the current native implementation, metadata-only queries disable auth UI and can end up omitting/returning nothing for biometric-protected entries (Keychain may respond with
errSecInteractionNotAllowed). Consider clarifying here what callers should expect (e.g., protected items may not be listable withoutincludeValues: true, andhasItemis the reliable silent existence probe).