Skip to content

Commit 240bc60

Browse files
committed
fix(ios): prompt simulator biometric auth before keychain fetch and probe security on main thread
- Invoke simulator biometric prompt (when needed) before calling SecItemCopyMatching so simulator flows mirror device auth and cancellation maps to a consistent RuntimeError. - Add performSimulatorBiometricPromptIfNeeded helper that evaluates LAContext on the main thread and surfaces user-cancel errors. - Ensure SecurityAvailabilityResolver probes capabilities on the main thread and correctly distinguishes simulator biometry/secure-enclave availability. - Update example Podfile.lock: SensitiveInfo -> 6.0.0-rc.10 and CocoaPods -> 1.16.2.
1 parent a3b9cda commit 240bc60

File tree

3 files changed

+84
-9
lines changed

3 files changed

+84
-9
lines changed

example/ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2437,7 +2437,7 @@ PODS:
24372437
- React-perflogger (= 0.82.1)
24382438
- React-utils (= 0.82.1)
24392439
- SocketRocket
2440-
- SensitiveInfo (6.0.0-rc.8):
2440+
- SensitiveInfo (6.0.0-rc.10):
24412441
- boost
24422442
- DoubleConversion
24432443
- fast_float
@@ -2784,10 +2784,10 @@ SPEC CHECKSUMS:
27842784
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
27852785
ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9
27862786
ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424
2787-
SensitiveInfo: de10b692c50b4dfaf81507b23470ad0c616f7f5e
2787+
SensitiveInfo: 929dfd44a2b79e7f040d3e1e134bef749d2c8cee
27882788
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
27892789
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
27902790

27912791
PODFILE CHECKSUM: 7ee3efea19ddd1156f9f61f93fc84a48ff536985
27922792

2793-
COCOAPODS: 1.15.2
2793+
COCOAPODS: 1.16.2

ios/HybridSensitiveInfo.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ final class HybridSensitiveInfo: HybridSensitiveInfoSpec {
266266
}
267267

268268
private func copyMatching(query: [String: Any], prompt: AuthenticationPrompt?) throws -> AnyObject? {
269+
#if targetEnvironment(simulator)
270+
try performSimulatorBiometricPromptIfNeeded(prompt: prompt)
271+
#endif
269272
var result: CFTypeRef?
270273
var status = performCopyMatching(query as CFDictionary, result: &result)
271274

@@ -405,4 +408,46 @@ final class HybridSensitiveInfo: HybridSensitiveInfoSpec {
405408
}
406409
return status
407410
}
411+
412+
#if targetEnvironment(simulator)
413+
private func performSimulatorBiometricPromptIfNeeded(prompt: AuthenticationPrompt?) throws {
414+
guard let prompt else { return }
415+
416+
let context = makeLAContext(prompt: prompt)
417+
var error: NSError?
418+
419+
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error),
420+
context.biometryType != .none else {
421+
return
422+
}
423+
424+
let reason = prompt.description ?? prompt.title ?? "Authenticate to continue"
425+
let semaphore = DispatchSemaphore(value: 0)
426+
var evaluationError: Error?
427+
428+
DispatchQueue.main.async {
429+
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, policyError in
430+
if !success {
431+
evaluationError = policyError
432+
}
433+
semaphore.signal()
434+
}
435+
}
436+
437+
semaphore.wait()
438+
439+
if let evaluationError {
440+
if let laError = evaluationError as? LAError {
441+
switch laError.code {
442+
case .userCancel, .userFallback, .systemCancel:
443+
throw RuntimeError.error(withMessage: "[E_AUTH_CANCELED] Authentication prompt canceled by the user.")
444+
default:
445+
break
446+
}
447+
}
448+
449+
throw RuntimeError.error(withMessage: "Keychain fetch failed: \(evaluationError.localizedDescription)")
450+
}
451+
}
452+
#endif
408453
}

ios/Internal/Security/SecurityAvailabilityResolver.swift

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,50 @@ final class SecurityAvailabilityResolver {
2020
return cachedValue
2121
}
2222

23+
let snapshot = resolveOnMainThread()
24+
cached = snapshot
25+
return snapshot
26+
}
27+
28+
private func resolveOnMainThread() -> (secureEnclave: Bool, strongBox: Bool, biometry: Bool, deviceCredential: Bool) {
29+
if Thread.isMainThread {
30+
return performCapabilityProbe()
31+
}
32+
33+
var snapshot: (secureEnclave: Bool, strongBox: Bool, biometry: Bool, deviceCredential: Bool) = (
34+
secureEnclave: false,
35+
strongBox: false,
36+
biometry: false,
37+
deviceCredential: false
38+
)
39+
40+
DispatchQueue.main.sync {
41+
snapshot = performCapabilityProbe()
42+
}
43+
44+
return snapshot
45+
}
46+
47+
private func performCapabilityProbe() -> (secureEnclave: Bool, strongBox: Bool, biometry: Bool, deviceCredential: Bool) {
2348
let context = LAContext()
2449
var error: NSError?
2550

26-
let supportsBiometry = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) && context.biometryType != .none
51+
let supportsBiometry = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
52+
#if targetEnvironment(simulator)
53+
let biometryAvailable = supportsBiometry
54+
let secureEnclaveAvailable = supportsBiometry
55+
#else
56+
let biometryAvailable = supportsBiometry && context.biometryType != .none
57+
let secureEnclaveAvailable = biometryAvailable
58+
#endif
2759
error = nil
2860
let supportsDeviceCredential = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
2961

30-
let snapshot = (
31-
secureEnclave: supportsBiometry,
62+
return (
63+
secureEnclave: secureEnclaveAvailable,
3264
strongBox: false,
33-
biometry: supportsBiometry,
65+
biometry: biometryAvailable,
3466
deviceCredential: supportsDeviceCredential
3567
)
36-
cached = snapshot
37-
return snapshot
3868
}
3969
}

0 commit comments

Comments
 (0)