RadrootsAppleUserPresence.swift (8276B)
1 import Foundation 2 3 #if canImport(LocalAuthentication) 4 @preconcurrency import LocalAuthentication 5 #endif 6 7 public struct RadrootsAppleUserPresenceAdapters: Sendable { 8 public let currentStatus: @Sendable () async throws -> RadrootsUserPresenceStatus 9 public let verify: @Sendable (RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult 10 11 public init( 12 currentStatus: @escaping @Sendable () async throws -> RadrootsUserPresenceStatus, 13 verify: @escaping @Sendable (RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult 14 ) { 15 self.currentStatus = currentStatus 16 self.verify = verify 17 } 18 19 public static func live(callbackTimeout: TimeInterval = 30) -> Self { 20 #if canImport(LocalAuthentication) 21 Self( 22 currentStatus: { 23 Self.status(for: LAContext()) 24 }, 25 verify: { request in 26 let context = LAContext() 27 return try await Self.verify( 28 request, 29 context: context, 30 callbackTimeout: callbackTimeout 31 ) 32 } 33 ) 34 #else 35 Self( 36 currentStatus: { 37 throw RadrootsUserPresenceError.unavailable("user presence is unavailable") 38 }, 39 verify: { _ in 40 throw RadrootsUserPresenceError.unavailable("user presence is unavailable") 41 } 42 ) 43 #endif 44 } 45 } 46 47 public final class RadrootsAppleUserPresence: RadrootsUserPresence, Sendable { 48 private let adapters: RadrootsAppleUserPresenceAdapters 49 50 public init(adapters: RadrootsAppleUserPresenceAdapters = RadrootsAppleUserPresenceAdapters.live()) { 51 self.adapters = adapters 52 } 53 54 public func currentStatus() async throws -> RadrootsUserPresenceStatus { 55 try await adapters.currentStatus() 56 } 57 58 public func verify(_ request: RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult { 59 try await adapters.verify(request) 60 } 61 } 62 63 #if canImport(LocalAuthentication) 64 extension RadrootsAppleUserPresenceAdapters { 65 static func platformPolicy(_ policy: RadrootsUserPresencePolicy) -> LAPolicy { 66 switch policy { 67 case .deviceOwnerAuthentication: 68 .deviceOwnerAuthentication 69 case .deviceOwnerAuthenticationWithBiometrics: 70 .deviceOwnerAuthenticationWithBiometrics 71 } 72 } 73 74 static func status(for context: LAContext) -> RadrootsUserPresenceStatus { 75 var biometricsError: NSError? 76 let canEvaluateBiometrics = context.canEvaluatePolicy( 77 .deviceOwnerAuthenticationWithBiometrics, 78 error: &biometricsError 79 ) 80 81 var deviceCredentialError: NSError? 82 let canEvaluateDeviceCredential = context.canEvaluatePolicy( 83 .deviceOwnerAuthentication, 84 error: &deviceCredentialError 85 ) 86 87 let support: RadrootsUserPresenceSupport 88 if canEvaluateBiometrics { 89 support = .biometricsOrDeviceCredential 90 } else if canEvaluateDeviceCredential { 91 support = .deviceCredential 92 } else { 93 support = .none 94 } 95 96 return RadrootsUserPresenceStatus( 97 support: support, 98 biometryKind: biometryKind(context.biometryType), 99 canEvaluateDeviceCredential: canEvaluateDeviceCredential, 100 canEvaluateBiometrics: canEvaluateBiometrics 101 ) 102 } 103 104 static func biometryKind(_ biometryType: LABiometryType) -> RadrootsBiometryKind { 105 switch biometryType { 106 case .none: 107 .none 108 case .touchID: 109 .touchID 110 case .faceID: 111 .faceID 112 case .opticID: 113 .opticID 114 @unknown default: 115 .unknown 116 } 117 } 118 119 static func verify( 120 _ request: RadrootsUserPresenceRequest, 121 context: LAContext, 122 callbackTimeout: TimeInterval 123 ) async throws -> RadrootsUserPresenceResult { 124 try await RadrootsAppleUserPresenceAsyncSupport.awaitCallback( 125 timeout: callbackTimeout, 126 timeoutMessage: "timed out while completing user presence verification" 127 ) { completion in 128 context.evaluatePolicy( 129 platformPolicy(request.policy), 130 localizedReason: request.reason 131 ) { success, error in 132 if let error { 133 completion(.failure(adapt(error: error))) 134 } else { 135 completion(.success(RadrootsUserPresenceResult(policy: request.policy, verified: success))) 136 } 137 } 138 } 139 } 140 141 static func adapt(error: Error) -> RadrootsUserPresenceError { 142 if let error = error as? RadrootsUserPresenceError { 143 return error 144 } 145 146 if let error = error as? LAError { 147 switch error.code { 148 case .userCancel, .userFallback: 149 return .userCancelled(error.localizedDescription) 150 case .appCancel, .systemCancel, .notInteractive: 151 return .transientFailure(error.localizedDescription) 152 case .biometryNotAvailable, .biometryNotEnrolled, .passcodeNotSet: 153 return .unavailable(error.localizedDescription) 154 case .authenticationFailed: 155 return .permissionDenied(error.localizedDescription) 156 default: 157 return .permanentFailure(error.localizedDescription) 158 } 159 } 160 161 return .permanentFailure(error.localizedDescription) 162 } 163 } 164 #endif 165 166 enum RadrootsAppleUserPresenceAsyncSupport { 167 static func awaitCallback<Value: Sendable>( 168 timeout: TimeInterval, 169 timeoutMessage: String, 170 _ body: (@escaping @Sendable (Result<Value, RadrootsUserPresenceError>) -> Void) -> Void 171 ) async throws -> Value { 172 let state = RadrootsAppleUserPresenceAsyncCallbackState<Value>() 173 return try await withTaskCancellationHandler { 174 try await withCheckedThrowingContinuation { continuation in 175 state.install(continuation) 176 body { result in 177 state.resume(result) 178 } 179 Task { 180 try? await Task.sleep(nanoseconds: try Self.timeoutNanoseconds(timeout)) 181 state.resume(.failure(.timeout(timeoutMessage))) 182 } 183 } 184 } onCancel: { 185 state.resume(.failure(.userCancelled("user presence verification was cancelled"))) 186 } 187 } 188 189 private static func timeoutNanoseconds(_ timeout: TimeInterval) throws -> UInt64 { 190 guard timeout.isFinite, timeout > 0 else { 191 throw RadrootsUserPresenceError.invalidRequest("user presence timeout must be finite and greater than zero") 192 } 193 let nanoseconds = timeout * 1_000_000_000 194 guard nanoseconds <= Double(UInt64.max) else { 195 throw RadrootsUserPresenceError.invalidRequest("user presence timeout is too large") 196 } 197 return UInt64(nanoseconds) 198 } 199 } 200 201 private final class RadrootsAppleUserPresenceAsyncCallbackState<Value: Sendable>: @unchecked Sendable { 202 private let lock = NSLock() 203 private var continuation: CheckedContinuation<Value, any Error>? 204 private var didResolve = false 205 206 func install(_ continuation: CheckedContinuation<Value, any Error>) { 207 lock.lock() 208 defer { lock.unlock() } 209 guard !didResolve else { 210 continuation.resume(throwing: RadrootsUserPresenceError.transientFailure("user presence verification already resolved")) 211 return 212 } 213 self.continuation = continuation 214 } 215 216 func resume(_ result: Result<Value, RadrootsUserPresenceError>) { 217 let pending: CheckedContinuation<Value, any Error>? 218 lock.lock() 219 if didResolve { 220 lock.unlock() 221 return 222 } 223 didResolve = true 224 pending = continuation 225 continuation = nil 226 lock.unlock() 227 228 switch result { 229 case .success(let value): 230 pending?.resume(returning: value) 231 case .failure(let error): 232 pending?.resume(throwing: error) 233 } 234 } 235 }