Skip to content

Commit 4144224

Browse files
committed
Add keychain item for biometric registration id
1 parent 7eaec64 commit 4144224

File tree

2 files changed

+46
-26
lines changed

2 files changed

+46
-26
lines changed

Sources/StytchCore/KeychainClient/KeychainClient+Item.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ extension KeychainClient {
5959
}
6060

6161
extension KeychainClient.Item {
62+
// The private key registration is central to biometric authentication, and this item should be protected by biometrics unless explicitly specified otherwise by the caller.
6263
static let privateKeyRegistration: Self = .init(kind: .privateKey, name: "stytch_private_key_registration")
64+
// This was introduced in version 0.54.0 to store the biometric registration ID in a keychain item that is not protected by biometrics.
65+
static let biometricKeyRegistration: Self = .init(kind: .object, name: "stytch_biometric_key_registration")
6366

6467
static let sessionToken: Self = .init(kind: .token, name: SessionToken.Kind.opaque.name)
6568
static let sessionJwt: Self = .init(kind: .token, name: SessionToken.Kind.jwt.name)

Sources/StytchCore/StytchClient/Biometrics/StytchClient+Biometrics.swift

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public extension StytchClient {
2929

3030
/// Indicates if there is an existing biometric registration on device.
3131
public var registrationAvailable: Bool {
32-
keychainClient.valueExistsForItem(.privateKeyRegistration)
32+
keychainClient.valueExistsForItem(.biometricKeyRegistration)
3333
}
3434

3535
#if !os(tvOS) && !os(watchOS)
@@ -51,17 +51,17 @@ public extension StytchClient {
5151
// sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
5252
/// Removes the current device's existing biometric registration from both the device itself and from the server.
5353
public func removeRegistration() async throws {
54-
guard let queryResult: KeychainClient.QueryResult = try? keychainClient.get(.privateKeyRegistration).first else {
54+
guard let queryResult = try? keychainClient.get(.biometricKeyRegistration).first else {
5555
return
5656
}
5757

5858
// 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))
59+
if let biometricRegistrationId = queryResult.stringValue {
60+
_ = try await StytchClient.user.deleteFactor(.biometricRegistration(id: User.BiometricRegistration.ID(stringLiteral: biometricRegistrationId)))
6161
}
6262

63-
// Remove local registration
64-
try keychainClient.removeItem(.privateKeyRegistration)
63+
// Remove local registrations
64+
try clearBiometricRegistrations()
6565
}
6666

6767
// sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
@@ -104,23 +104,29 @@ public extension StytchClient {
104104
registrationId: finishResponse.biometricRegistrationId
105105
)
106106

107+
// Set the .privateKeyRegistration
107108
try keychainClient.set(
108109
key: privateKey,
109110
registration: registration,
110111
accessPolicy: parameters.accessPolicy.keychainValue
111112
)
112113

114+
// Set the .biometricKeyRegistration
115+
try keychainClient.set(registration.registrationId.rawValue, for: .biometricKeyRegistration)
116+
113117
return finishResponse
114118
}
115119

116120
// sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling)
117121
/// 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.
118122
public func authenticate(parameters: AuthenticateParameters) async throws -> AuthenticateResponse {
119-
guard let queryResult: KeychainClient.QueryResult = try keychainClient.get(.privateKeyRegistration).first else {
123+
guard let privateKeyRegistrationQueryResult: KeychainClient.QueryResult = try keychainClient.get(.privateKeyRegistration).first else {
120124
throw StytchSDKError.noBiometricRegistration
121125
}
122126

123-
let privateKey = queryResult.data
127+
try copyBiometricRegistrationIDToKeychainIfNeeded(privateKeyRegistrationQueryResult)
128+
129+
let privateKey = privateKeyRegistrationQueryResult.data
124130
let publicKey = try cryptoClient.publicKeyForPrivateKey(privateKey)
125131

126132
let startResponse: AuthenticateStartResponse = try await router.post(
@@ -143,33 +149,44 @@ public extension StytchClient {
143149
return authenticateResponse
144150
}
145151

152+
// Clear both the .privateKeyRegistration and the .biometricKeyRegistration
153+
func clearBiometricRegistrations() throws {
154+
try keychainClient.removeItem(.privateKeyRegistration)
155+
try keychainClient.removeItem(.biometricKeyRegistration)
156+
}
157+
158+
// if we have a local biometric registration that doesn't exist on the user object, delete the local
146159
func cleanupPotentiallyOrphanedBiometricRegistrations() {
147160
guard let user = StytchClient.user.getSync() else {
148161
return
149162
}
150163

151-
// if we have a local biometric registration that doesn't exist on the user object, delete the local
152164
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)
165+
try? clearBiometricRegistrations()
166+
} else {
167+
let queryResult = try? keychainClient.get(.biometricKeyRegistration).first
168+
169+
// Check if the user's biometric registrations contain the ID
170+
let userBiometricRegistrationIds = user.biometricRegistrations.compactMap(\.biometricRegistrationId.rawValue)
171+
if let biometricRegistrationId = queryResult?.stringValue, userBiometricRegistrationIds.contains(biometricRegistrationId) == false {
172+
// Remove the orphaned biometric registration
173+
try? clearBiometricRegistrations()
174+
}
157175
}
158176
}
159177

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)
178+
/*
179+
After introducing the .biometricKeyRegistration keychain item in version 0.54.0, we needed a way for versions prior to 0.54.0
180+
to copy the value stored in the biometrically protected .privateKeyRegistration keychain item into the non-biometric
181+
.biometricKeyRegistration keychain item without triggering unnecessary Face ID prompts. Since a Face ID prompt is already
182+
being shown here for authentication, we decided to use this as an opportunity to perform the migration by copying the
183+
registration ID into the .biometricKeyRegistration keychain item. For versions after 0.54.0, this action occurs during
184+
registration, and it should only happen here if the .biometricKeyRegistration keychain item is empty.
185+
*/
186+
func copyBiometricRegistrationIDToKeychainIfNeeded(_ queryResult: KeychainClient.QueryResult) throws {
187+
let biometricKeyRegistration = try? keychainClient.get(.biometricKeyRegistration).first
188+
if biometricKeyRegistration == nil, let registration = try queryResult.generic.map({ try jsonDecoder.decode(KeychainClient.KeyRegistration.self, from: $0) }) {
189+
try keychainClient.set(registration.registrationId.rawValue, for: .biometricKeyRegistration)
173190
}
174191
}
175192
}

0 commit comments

Comments
 (0)