Skip to content

Commit 261bb86

Browse files
PM-11883: Improve handling of biometric unlock errors (#917)
1 parent 3393c72 commit 261bb86

File tree

5 files changed

+73
-26
lines changed

5 files changed

+73
-26
lines changed

BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepository.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,9 @@ class DefaultBiometricsRepository: BiometricsRepository {
173173
kLAErrorUserFallback:
174174
throw BiometricsServiceError.biometryFailed
175175
default:
176-
throw BiometricsServiceError.getAuthKeyFailed
176+
throw error
177177
}
178178
}
179-
} catch {
180-
throw BiometricsServiceError.getAuthKeyFailed
181179
}
182180
}
183181
}

BitwardenShared/Core/Auth/Services/Biometrics/BiometricsRepositoryTests.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable:
350350
}
351351
}
352352

353-
/// `getUserAuthKey` retrieves the key from keychain and updates integrity state.
353+
/// `getUserAuthKey` throws an error if one occurs.
354354
func test_getUserAuthKey_unknownError() async throws {
355355
let active = Account.fixture()
356356
stateService.activeAccount = active
@@ -360,7 +360,22 @@ final class BiometricsRepositoryTests: BitwardenTestCase { // swiftlint:disable:
360360
active.profile.userId: true,
361361
]
362362
keychainService.getResult = .failure(BitwardenTestError.example)
363-
await assertAsyncThrows(error: BiometricsServiceError.getAuthKeyFailed) {
363+
await assertAsyncThrows(error: BitwardenTestError.example) {
364+
_ = try await subject.getUserAuthKey()
365+
}
366+
}
367+
368+
/// `getUserAuthKey` throws an error if an unknown OS error occurs.
369+
func test_getUserAuthKey_unknownOSError() async throws {
370+
let active = Account.fixture()
371+
stateService.activeAccount = active
372+
let integrity = Data("Face/Off".utf8)
373+
biometricsService.biometricIntegrityState = integrity
374+
stateService.biometricsEnabled = [
375+
active.profile.userId: true,
376+
]
377+
keychainService.getResult = .failure(KeychainServiceError.osStatusError(errSecParam))
378+
await assertAsyncThrows(error: KeychainServiceError.osStatusError(errSecParam)) {
364379
_ = try await subject.getUserAuthKey()
365380
}
366381
}

BitwardenShared/Core/Platform/Services/ErrorReporter/BitwardenError.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@ enum BitwardenError {
4545
/// - Parameters:
4646
/// - type: The type of error. This is used to group the errors in the Crashlytics dashboard.
4747
/// - message: A message describing the error that occurred.
48+
/// - error: An optional underlying error that caused the error.
4849
///
49-
static func generalError(type: String, message: String) -> NSError {
50-
NSError(
50+
static func generalError(type: String, message: String, error: Error? = nil) -> NSError {
51+
var userInfo: [String: Any] = ["ErrorMessage": message]
52+
if let error {
53+
userInfo[NSUnderlyingErrorKey] = error
54+
}
55+
return NSError(
5156
domain: "General Error: \(type)",
5257
code: Code.generalError.rawValue,
53-
userInfo: [
54-
"ErrorMessage": message,
55-
]
58+
userInfo: userInfo
5659
)
5760
}
5861

BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessor.swift

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -243,29 +243,32 @@ class VaultUnlockProcessor: StateProcessor<
243243
await coordinator.handleEvent(.didCompleteAuth)
244244
state.unsuccessfulUnlockAttemptsCount = 0
245245
await services.stateService.setUnsuccessfulUnlockAttempts(0)
246-
} catch let error as BiometricsServiceError {
247-
Logger.processor.error("BiometricsServiceError unlocking vault with biometrics: \(error)")
246+
} catch BiometricsServiceError.biometryCancelled {
247+
Logger.processor.error("Biometric unlock cancelled.")
248+
// Do nothing if the user cancels.
249+
} catch BiometricsServiceError.biometryLocked {
250+
Logger.processor.error("Biometric unlock failed duo to biometrics lockout.")
248251
// If the user has locked biometry, logout immediately.
249-
if case .biometryLocked = error {
250-
await logoutUser(userInitiated: true)
251-
return
252-
}
253-
if case .biometryCancelled = error {
254-
// Do nothing if the user cancels.
255-
return
256-
}
257-
// There is no biometric auth key stored, set user preference to false.
258-
if case .getAuthKeyFailed = error {
259-
try? await services.authRepository.allowBioMetricUnlock(false)
260-
}
252+
await logoutUser(userInitiated: true)
253+
} catch BiometricsServiceError.getAuthKeyFailed {
254+
services.errorReporter.log(error: BitwardenError.generalError(
255+
type: "VaultUnlock: Get Biometrics Auth Key Failed",
256+
message: "Biometrics auth is enabled but key was unable to be found. Disabling biometric unlock."
257+
))
258+
try? await services.authRepository.allowBioMetricUnlock(false)
261259
await loadData()
262260
} catch let error as StateServiceError {
263261
// If there is no active account, don't add to the unsuccessful count.
264-
Logger.processor.error("StateServiceError unlocking vault with biometrics: \(error)")
262+
services.errorReporter.log(error: error)
265263
// Just send the user back to landing.
266264
coordinator.navigate(to: .landing)
267265
} catch {
268-
Logger.processor.error("Error unlocking vault with biometrics: \(error)")
266+
services.errorReporter.log(error: BitwardenError.generalError(
267+
type: "VaultUnlock: Biometrics Unlock Error",
268+
message: "A biometrics error occurred.",
269+
error: error
270+
))
271+
269272
state.unsuccessfulUnlockAttemptsCount += 1
270273
await services.stateService
271274
.setUnsuccessfulUnlockAttempts(state.unsuccessfulUnlockAttemptsCount)

BitwardenShared/UI/Auth/VaultUnlock/VaultUnlockProcessorTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,21 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t
487487
XCTAssertEqual(attemptsInUserDefaults, 0)
488488
}
489489

490+
/// `perform(_:)` with `.unlockVaultWithBiometrics` logs the user out if biometrics is locked
491+
/// due to too many failed attempts.
492+
@MainActor
493+
func test_perform_unlockWithBiometrics_biometryLocked() async throws {
494+
stateService.activeAccount = .fixture()
495+
biometricsRepository.biometricUnlockStatus = .success(
496+
.available(.touchID, enabled: true, hasValidIntegrity: true)
497+
)
498+
authRepository.unlockVaultWithBiometricsResult = .failure(BiometricsServiceError.biometryLocked)
499+
500+
await subject.perform(.unlockVaultWithBiometrics)
501+
XCTAssertNil(coordinator.routes.last)
502+
XCTAssertEqual(coordinator.events, [.action(.logout(userId: nil, userInitiated: true))])
503+
}
504+
490505
/// `perform(_:)` with `.unlockVaultWithBiometrics` shows the KDF warning in an extension if the
491506
/// KDF memory is potentially too high.
492507
@MainActor
@@ -521,6 +536,7 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t
521536
await subject.perform(.unlockVaultWithBiometrics)
522537
let route = try XCTUnwrap(coordinator.routes.last)
523538
XCTAssertEqual(route, .landing)
539+
XCTAssertEqual(errorReporter.errors as? [StateServiceError], [.noActiveAccount])
524540
}
525541

526542
/// `perform(_:)` with `.unlockWithBiometrics` requires a set user preference.
@@ -573,6 +589,12 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t
573589
await subject.perform(.unlockVaultWithBiometrics)
574590
XCTAssertNil(coordinator.routes.last)
575591
XCTAssertEqual(1, subject.state.unsuccessfulUnlockAttemptsCount)
592+
593+
XCTAssertEqual(errorReporter.errors.count, 1)
594+
XCTAssertEqual(
595+
(errorReporter.errors[0] as NSError).domain,
596+
"General Error: VaultUnlock: Biometrics Unlock Error"
597+
)
576598
}
577599

578600
/// `perform(_:)` with `.unlockWithBiometrics` requires successful biometrics.
@@ -594,6 +616,12 @@ class VaultUnlockProcessorTests: BitwardenTestCase { // swiftlint:disable:this t
594616
.logout(userId: nil, userInitiated: true)
595617
)
596618
)
619+
620+
XCTAssertEqual(errorReporter.errors.count, 1)
621+
XCTAssertEqual(
622+
(errorReporter.errors[0] as NSError).domain,
623+
"General Error: VaultUnlock: Biometrics Unlock Error"
624+
)
597625
}
598626

599627
/// `perform(_:)` with `.unlockWithBiometrics` requires successful biometrics.

0 commit comments

Comments
 (0)