@@ -9,6 +9,11 @@ public extension StytchClient.Biometrics {
99 case availableRegistered
1010 }
1111}
12+
13+ public extension StytchClient {
14+ /// The interface for interacting with biometrics products.
15+ static var biometrics : Biometrics { . init( router: router. scopedRouter { $0. biometrics } ) }
16+ }
1217#endif
1318
1419public extension StytchClient {
@@ -29,7 +34,7 @@ public extension StytchClient {
2934
3035 /// Indicates if there is an existing biometric registration on device.
3136 public var registrationAvailable : Bool {
32- keychainClient. valueExistsForItem ( . privateKeyRegistration )
37+ keychainClient. valueExistsForItem ( . biometricKeyRegistration )
3338 }
3439
3540 #if !os(tvOS) && !os(watchOS)
@@ -51,17 +56,17 @@ public extension StytchClient {
5156 // sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
5257 /// Removes the current device's existing biometric registration from both the device itself and from the server.
5358 public func removeRegistration( ) async throws {
54- guard let queryResult: KeychainClient . QueryResult = try ? keychainClient. get ( . privateKeyRegistration ) . first else {
59+ guard let queryResult = try ? keychainClient. get ( . biometricKeyRegistration ) . first else {
5560 return
5661 }
5762
5863 // Delete registration from backend
59- if let registration = try queryResult. generic . map ( { try jsonDecoder . decode ( KeychainClient . KeyRegistration . self , from : $0 ) } ) {
60- _ = try await StytchClient . user. deleteFactor ( . biometricRegistration( id: registration . registrationId ) )
64+ if let biometricRegistrationId = queryResult. stringValue {
65+ _ = try await StytchClient . user. deleteFactor ( . biometricRegistration( id: User . BiometricRegistration . ID ( stringLiteral : biometricRegistrationId ) ) )
6166 }
6267
63- // Remove local registration
64- try keychainClient . removeItem ( . privateKeyRegistration )
68+ // Remove local registrations
69+ try clearBiometricRegistrations ( )
6570 }
6671
6772 // sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
@@ -104,23 +109,29 @@ public extension StytchClient {
104109 registrationId: finishResponse. biometricRegistrationId
105110 )
106111
112+ // Set the .privateKeyRegistration
107113 try keychainClient. set (
108114 key: privateKey,
109115 registration: registration,
110116 accessPolicy: parameters. accessPolicy. keychainValue
111117 )
112118
119+ // Set the .biometricKeyRegistration
120+ try keychainClient. set ( registration. registrationId. rawValue, for: . biometricKeyRegistration)
121+
113122 return finishResponse
114123 }
115124
116125 // sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
117126 /// If a valid biometric registration exists, this method confirms the current device owner via the device's built-in biometric reader and returns an updated session object by either starting a new session or adding a the biometric factor to an existing session.
118127 public func authenticate( parameters: AuthenticateParameters ) async throws -> AuthenticateResponse {
119- guard let queryResult : KeychainClient . QueryResult = try keychainClient. get ( . privateKeyRegistration) . first else {
128+ guard let privateKeyRegistrationQueryResult : KeychainClient . QueryResult = try keychainClient. get ( . privateKeyRegistration) . first else {
120129 throw StytchSDKError . noBiometricRegistration
121130 }
122131
123- let privateKey = queryResult. data
132+ try copyBiometricRegistrationIDToKeychainIfNeeded ( privateKeyRegistrationQueryResult)
133+
134+ let privateKey = privateKeyRegistrationQueryResult. data
124135 let publicKey = try cryptoClient. publicKeyForPrivateKey ( privateKey)
125136
126137 let startResponse : AuthenticateStartResponse = try await router. post (
@@ -143,45 +154,53 @@ public extension StytchClient {
143154 return authenticateResponse
144155 }
145156
157+ // Clear both the .privateKeyRegistration and the .biometricKeyRegistration
158+ func clearBiometricRegistrations( ) throws {
159+ try keychainClient. removeItem ( . privateKeyRegistration)
160+ try keychainClient. removeItem ( . biometricKeyRegistration)
161+ }
162+
163+ // if we have a local biometric registration that doesn't exist on the user object, delete the local
146164 func cleanupPotentiallyOrphanedBiometricRegistrations( ) {
147165 guard let user = StytchClient . user. getSync ( ) else {
148166 return
149167 }
150168
151- // if we have a local biometric registration that doesn't exist on the user object, delete the local
152169 if user. biometricRegistrations. isEmpty {
153- try ? keychainClient. removeItem ( . privateKeyRegistration)
154- } else if !user. biometricRegistrations. isEmpty {
155- let queryResult = try ? keychainClient. get ( . privateKeyRegistration) . first
156- cleanupBiometricRegistrationIfOrphaned ( queryResult: queryResult, user: user)
170+ try ? clearBiometricRegistrations ( )
171+ } else {
172+ let queryResult = try ? keychainClient. get ( . biometricKeyRegistration) . first
173+
174+ // Check if the user's biometric registrations contain the ID
175+ var userBiometricRegistrationIds = [ String] ( )
176+ for biometricRegistration in user. biometricRegistrations {
177+ userBiometricRegistrationIds. append ( biometricRegistration. biometricRegistrationId. rawValue)
178+ }
179+
180+ if let biometricRegistrationId = queryResult? . stringValue, userBiometricRegistrationIds. contains ( biometricRegistrationId) == false {
181+ // Remove the orphaned biometric registration
182+ try ? clearBiometricRegistrations ( )
183+ }
157184 }
158185 }
159186
160- private func cleanupBiometricRegistrationIfOrphaned(
161- queryResult: KeychainClient . QueryResult ? ,
162- user: User
163- ) {
164- // Decode the biometric registration ID from the query result
165- let biometricRegistrationId = try ? queryResult? . generic. map {
166- try jsonDecoder. decode ( KeychainClient . KeyRegistration. self, from: $0)
167- }
168-
169- // Check if the user's biometric registrations contain the ID
170- if user. biometricRegistrations. map ( \. id) . contains ( biometricRegistrationId? . registrationId) == false {
171- // Remove the orphaned biometric registration
172- try ? keychainClient. removeItem ( . privateKeyRegistration)
187+ /*
188+ After introducing the .biometricKeyRegistration keychain item in version 0.54.0, we needed a way for versions prior to 0.54.0
189+ to copy the value stored in the biometrically protected .privateKeyRegistration keychain item into the non-biometric
190+ .biometricKeyRegistration keychain item without triggering unnecessary Face ID prompts. Since a Face ID prompt is already
191+ being shown here for authentication, we decided to use this as an opportunity to perform the migration by copying the
192+ registration ID into the .biometricKeyRegistration keychain item. For versions after 0.54.0, this action occurs during
193+ registration, and it should only happen here if the .biometricKeyRegistration keychain item is empty.
194+ */
195+ func copyBiometricRegistrationIDToKeychainIfNeeded( _ queryResult: KeychainClient . QueryResult ) throws {
196+ let biometricKeyRegistration = try ? keychainClient. get ( . biometricKeyRegistration) . first
197+ if biometricKeyRegistration == nil , let registration = try queryResult. generic. map ( { try jsonDecoder. decode ( KeychainClient . KeyRegistration. self, from: $0) } ) {
198+ try keychainClient. set ( registration. registrationId. rawValue, for: . biometricKeyRegistration)
173199 }
174200 }
175201 }
176202}
177203
178- #if !os(tvOS) && !os(watchOS)
179- public extension StytchClient {
180- /// The interface for interacting with biometrics products.
181- static var biometrics : Biometrics { . init( router: router. scopedRouter { $0. biometrics } ) }
182- }
183- #endif
184-
185204public extension StytchClient . Biometrics {
186205 typealias RegisterCompleteResponse = Response < RegisterCompleteResponseData >
187206
0 commit comments