Add support for Face ID/Touch ID to app lock. (#1966)

* Fix biometrics with low grace period and backgrounding before unlocked.

* Trigger permissions alert when enabling Face ID.
This commit is contained in:
Doug
2023-10-27 10:09:12 +01:00
committed by GitHub
parent 2eb8945824
commit 11563c6297
27 changed files with 598 additions and 92 deletions
@@ -291,7 +291,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
if oldVersion < Version(1, 1, 0) {
MXLog.info("Migrating to v1.1.0, signing out the user.")
// Version 1.1.0 switched the Rust crypto store to SQLite
// There are no migrations in place so we need to reset everything
// There are no migrations in place so we need to sign the user out
wipeUserData()
}
@@ -308,6 +308,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
private func wipeUserData(includingSettings: Bool = false) {
if includingSettings {
AppSettings.reset()
appLockFlowCoordinator.appLockService.disable()
}
userSessionStore.reset()
}
@@ -133,9 +133,6 @@ final class AppSettings {
/// The number of attempts the user has made to unlock the app with a PIN code (resets when unlocked).
@UserPreference(key: UserDefaultsKeys.appLockNumberOfPINAttempts, defaultValue: 0, storageType: .userDefaults(store))
var appLockNumberOfPINAttempts: Int
/// The number of attempts the user has made to unlock the app with Touch/Face ID (resets when unlocked).
@UserPreference(key: UserDefaultsKeys.appLockNumberOfBiometricAttempts, defaultValue: 0, storageType: .userDefaults(store))
var appLockNumberOfBiometricAttempts: Int
// MARK: - Authentication
@@ -32,6 +32,8 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
let userIndicatorController: UserIndicatorController
let navigationCoordinator: NavigationRootCoordinator
/// A task used to await biometric unlock before showing the PIN screen.
@CancellableTask private var unlockTask: Task<Void, Never>?
private var cancellables: Set<AnyCancellable> = []
private let actionsSubject: PassthroughSubject<AppLockFlowCoordinatorAction, Never> = .init()
@@ -44,6 +46,9 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
self.userIndicatorController = userIndicatorController
self.navigationCoordinator = navigationCoordinator
// Set the initial background state.
showPlaceholder()
appLockService.disabledPublisher
.sink { [weak self] _ in
// When the service is disabled via a force logout, we need to remove the activity indicator.
@@ -71,6 +76,8 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
// MARK: - App unlock
private func applicationDidEnterBackground() {
unlockTask = nil
guard appLockService.isEnabled else { return }
appLockService.applicationDidEnterBackground()
@@ -80,12 +87,33 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
private func applicationWillEnterForeground() {
guard appLockService.isEnabled else { return }
if appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) {
showUnlockScreen()
} else {
guard appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) else {
// Reveal the app again if within the grace period.
actionsSubject.send(.unlockApp)
return
}
// Show the relevant unlock mechanism.
unlockTask = Task { [weak self] in
guard let self else { return }
await startUnlockFlow()
}
}
/// Runs the unlock flow, showing Touch ID/Face ID if available, transitioning to PIN unlock if it fails or isn't available.
private func startUnlockFlow() async {
if appLockService.biometricUnlockEnabled, appLockService.biometricUnlockTrusted {
showPlaceholder() // For the unlock background.
if await appLockService.unlockWithBiometrics() {
actionsSubject.send(.unlockApp)
return
}
}
guard !Task.isCancelled else { return }
showUnlockScreen()
}
/// Displays the unlock flow with the app's placeholder view to hide obscure the view hierarchy in the app switcher.
@@ -140,6 +140,9 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
showBiometricsPrompt()
case (.createPIN, .settings):
navigationStackCoordinator.setSheetCoordinator(nil)
// The above is fine for change pin, but not create PIN when biometrics are unavailable.
// Need to track the two flows differently to call the alternative below.
// showSettings()
case (.biometricsPrompt, .settings):
showSettings()
case (.settings, .createPIN):
+4
View File
@@ -82,6 +82,8 @@ public enum L10n {
public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") }
/// End poll
public static var actionEndPoll: String { return L10n.tr("Localizable", "action_end_poll") }
/// Enter PIN
public static var actionEnterPin: String { return L10n.tr("Localizable", "action_enter_pin") }
/// Forgot password?
public static var actionForgotPassword: String { return L10n.tr("Localizable", "action_forgot_password") }
/// Forward
@@ -676,6 +678,8 @@ public enum L10n {
public static var screenAppLockBiometricAuthentication: String { return L10n.tr("Localizable", "screen_app_lock_biometric_authentication") }
/// biometric unlock
public static var screenAppLockBiometricUnlock: String { return L10n.tr("Localizable", "screen_app_lock_biometric_unlock") }
/// Authentication is needed to access your app
public static var screenAppLockBiometricUnlockReasonIos: String { return L10n.tr("Localizable", "screen_app_lock_biometric_unlock_reason_ios") }
/// Forgot PIN?
public static var screenAppLockForgotPin: String { return L10n.tr("Localizable", "screen_app_lock_forgot_pin") }
/// Change PIN code
@@ -137,6 +137,11 @@ class AppLockServiceMock: AppLockServiceProtocol {
set(value) { underlyingBiometricUnlockEnabled = value }
}
var underlyingBiometricUnlockEnabled: Bool!
var biometricUnlockTrusted: Bool {
get { return underlyingBiometricUnlockTrusted }
set(value) { underlyingBiometricUnlockTrusted = value }
}
var underlyingBiometricUnlockTrusted: Bool!
var disabledPublisher: AnyPublisher<Void, Never> {
get { return underlyingDisabledPublisher }
set(value) { underlyingDisabledPublisher = value }
@@ -147,11 +152,6 @@ class AppLockServiceMock: AppLockServiceProtocol {
set(value) { underlyingNumberOfPINAttempts = value }
}
var underlyingNumberOfPINAttempts: AnyPublisher<Int, Never>!
var numberOfBiometricAttempts: AnyPublisher<Int, Never> {
get { return underlyingNumberOfBiometricAttempts }
set(value) { underlyingNumberOfBiometricAttempts = value }
}
var underlyingNumberOfBiometricAttempts: AnyPublisher<Int, Never>!
//MARK: - setupPINCode
@@ -195,6 +195,35 @@ class AppLockServiceMock: AppLockServiceProtocol {
return validateReturnValue
}
}
//MARK: - enableBiometricUnlock
var enableBiometricUnlockCallsCount = 0
var enableBiometricUnlockCalled: Bool {
return enableBiometricUnlockCallsCount > 0
}
var enableBiometricUnlockReturnValue: Result<Void, AppLockServiceError>!
var enableBiometricUnlockClosure: (() -> Result<Void, AppLockServiceError>)?
func enableBiometricUnlock() -> Result<Void, AppLockServiceError> {
enableBiometricUnlockCallsCount += 1
if let enableBiometricUnlockClosure = enableBiometricUnlockClosure {
return enableBiometricUnlockClosure()
} else {
return enableBiometricUnlockReturnValue
}
}
//MARK: - disableBiometricUnlock
var disableBiometricUnlockCallsCount = 0
var disableBiometricUnlockCalled: Bool {
return disableBiometricUnlockCallsCount > 0
}
var disableBiometricUnlockClosure: (() -> Void)?
func disableBiometricUnlock() {
disableBiometricUnlockCallsCount += 1
disableBiometricUnlockClosure?()
}
//MARK: - disable
var disableCallsCount = 0
@@ -268,12 +297,12 @@ class AppLockServiceMock: AppLockServiceProtocol {
return unlockWithBiometricsCallsCount > 0
}
var unlockWithBiometricsReturnValue: Bool!
var unlockWithBiometricsClosure: (() -> Bool)?
var unlockWithBiometricsClosure: (() async -> Bool)?
func unlockWithBiometrics() -> Bool {
func unlockWithBiometrics() async -> Bool {
unlockWithBiometricsCallsCount += 1
if let unlockWithBiometricsClosure = unlockWithBiometricsClosure {
return unlockWithBiometricsClosure()
return await unlockWithBiometricsClosure()
} else {
return unlockWithBiometricsReturnValue
}
@@ -906,6 +935,72 @@ class KeychainControllerMock: KeychainControllerProtocol {
removePINCodeCallsCount += 1
removePINCodeClosure?()
}
//MARK: - containsPINCodeBiometricState
var containsPINCodeBiometricStateCallsCount = 0
var containsPINCodeBiometricStateCalled: Bool {
return containsPINCodeBiometricStateCallsCount > 0
}
var containsPINCodeBiometricStateReturnValue: Bool!
var containsPINCodeBiometricStateClosure: (() -> Bool)?
func containsPINCodeBiometricState() -> Bool {
containsPINCodeBiometricStateCallsCount += 1
if let containsPINCodeBiometricStateClosure = containsPINCodeBiometricStateClosure {
return containsPINCodeBiometricStateClosure()
} else {
return containsPINCodeBiometricStateReturnValue
}
}
//MARK: - setPINCodeBiometricState
var setPINCodeBiometricStateThrowableError: Error?
var setPINCodeBiometricStateCallsCount = 0
var setPINCodeBiometricStateCalled: Bool {
return setPINCodeBiometricStateCallsCount > 0
}
var setPINCodeBiometricStateReceivedState: Data?
var setPINCodeBiometricStateReceivedInvocations: [Data] = []
var setPINCodeBiometricStateClosure: ((Data) throws -> Void)?
func setPINCodeBiometricState(_ state: Data) throws {
if let error = setPINCodeBiometricStateThrowableError {
throw error
}
setPINCodeBiometricStateCallsCount += 1
setPINCodeBiometricStateReceivedState = state
setPINCodeBiometricStateReceivedInvocations.append(state)
try setPINCodeBiometricStateClosure?(state)
}
//MARK: - pinCodeBiometricState
var pinCodeBiometricStateCallsCount = 0
var pinCodeBiometricStateCalled: Bool {
return pinCodeBiometricStateCallsCount > 0
}
var pinCodeBiometricStateReturnValue: Data?
var pinCodeBiometricStateClosure: (() -> Data?)?
func pinCodeBiometricState() -> Data? {
pinCodeBiometricStateCallsCount += 1
if let pinCodeBiometricStateClosure = pinCodeBiometricStateClosure {
return pinCodeBiometricStateClosure()
} else {
return pinCodeBiometricStateReturnValue
}
}
//MARK: - removePINCodeBiometricState
var removePINCodeBiometricStateCallsCount = 0
var removePINCodeBiometricStateCalled: Bool {
return removePINCodeBiometricStateCallsCount > 0
}
var removePINCodeBiometricStateClosure: (() -> Void)?
func removePINCodeBiometricState() {
removePINCodeBiometricStateCallsCount += 1
removePINCodeBiometricStateClosure?()
}
}
class MediaPlayerMock: MediaPlayerProtocol {
var mediaSource: MediaSourceProxy?
@@ -46,11 +46,7 @@ class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModel
switch viewAction {
case .submitPINCode:
guard appLockService.unlock(with: state.bindings.pinCode) else {
handleInvalidPIN()
return
}
actionsSubject.send(.appUnlocked)
submitPINCode()
case .clearPINCode:
state.bindings.pinCode = ""
case .forgotPIN:
@@ -60,6 +56,14 @@ class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModel
// MARK: - Private
private func submitPINCode() {
guard appLockService.unlock(with: state.bindings.pinCode) else {
handleInvalidPIN()
return
}
actionsSubject.send(.appUnlocked)
}
private func handleForgotPIN() {
state.bindings.alertInfo = .init(id: .confirmResetPIN,
title: L10n.screenAppLockSignoutAlertTitle,
@@ -39,13 +39,34 @@ class AppLockSetupBiometricsScreenViewModel: AppLockSetupBiometricsScreenViewMod
switch viewAction {
case .allow:
MXLog.info("Enable biometric unlock.")
appLockService.biometricUnlockEnabled = true
actionsSubject.send(.continue)
Task { await enableBiometricUnlock() }
case .skip:
MXLog.info("Disable biometric unlock.")
appLockService.biometricUnlockEnabled = false
actionsSubject.send(.continue)
disableBiometricUnlock()
}
}
// MARK: - Private
private func enableBiometricUnlock() async {
guard case .success = appLockService.enableBiometricUnlock() else {
MXLog.error("Enabling biometric unlock failed.")
return
}
MXLog.info("Biometric unlock enabled.")
// Attempt unlock to trigger Face ID permissions alert.
if appLockService.biometryType == .faceID,
await !appLockService.unlockWithBiometrics() {
MXLog.info("Confirmation failed. Disabling biometric unlock.")
appLockService.disableBiometricUnlock()
}
actionsSubject.send(.continue)
}
private func disableBiometricUnlock() {
appLockService.disableBiometricUnlock()
MXLog.info("Biometric unlock disabled.")
actionsSubject.send(.continue)
}
}
@@ -30,8 +30,8 @@ struct AppLockSetupSettingsScreenViewState: BindableState {
let biometryType: LABiometryType
var bindings: AppLockSetupSettingsScreenViewStateBindings
var supportsBiometry: Bool { biometryType != .none }
var enableBiometryTitle: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) }
var supportsBiometrics: Bool { biometryType != .none }
var enableBiometricsTitle: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) }
}
struct AppLockSetupSettingsScreenViewStateBindings {
@@ -45,12 +45,34 @@ class AppLockSetupSettingsScreenViewModel: AppLockSetupSettingsScreenViewModelTy
case .disable:
showRemovePINAlert()
case .enableBiometricsChanged:
appLockService.biometricUnlockEnabled = state.bindings.enableBiometrics
Task { await toggleBiometrics() }
}
}
// MARK: - Private
private func toggleBiometrics() async {
if state.bindings.enableBiometrics {
guard case .success = appLockService.enableBiometricUnlock() else {
MXLog.error("Enabling biometric unlock failed.")
state.bindings.enableBiometrics = false
return
}
MXLog.info("Biometric unlock enabled.")
// Attempt unlock to trigger Face ID permissions alert.
if appLockService.biometryType == .faceID,
await !appLockService.unlockWithBiometrics() {
MXLog.info("Confirmation failed. Disabling biometric unlock.")
state.bindings.enableBiometrics = false
appLockService.disableBiometricUnlock()
}
} else {
appLockService.disableBiometricUnlock()
MXLog.info("Biometric unlock disabled.")
}
}
/// Shows a confirmation alert to the user before removing their PIN code.
private func showRemovePINAlert() {
state.bindings.alertInfo = .init(id: .confirmRemovePINCode,
@@ -32,9 +32,9 @@ struct AppLockSetupSettingsScreen: View {
}
}
if context.viewState.supportsBiometry {
if context.viewState.supportsBiometrics {
Section {
ListRow(label: .plain(title: context.viewState.enableBiometryTitle),
ListRow(label: .plain(title: context.viewState.enableBiometricsTitle),
kind: .toggle($context.enableBiometrics))
.onChange(of: context.enableBiometrics) { _ in
context.send(viewAction: .enableBiometricsChanged)
@@ -22,7 +22,7 @@ extension LABiometryType {
var systemSymbol: SFSymbol {
switch self {
case .none:
MXLog.error("Invalid presentation: Biometry not supported.")
MXLog.error("Invalid presentation: Biometrics not supported.")
return .viewfinder
case .touchID:
return .touchid
@@ -40,7 +40,7 @@ extension LABiometryType {
var localizedString: String {
switch self {
case .none:
MXLog.error("Invalid presentation: Biometry not supported.")
MXLog.error("Invalid presentation: Biometrics not supported.")
return L10n.screenAppLockBiometricUnlock
case .touchID:
return L10n.commonTouchIdIos
@@ -21,9 +21,10 @@ import LocalAuthentication
class AppLockService: AppLockServiceProtocol {
private let keychainController: KeychainControllerProtocol
private let appSettings: AppSettings
private let context = LAContext()
private let context: LAContext
private let timer: AppLockTimer
private let unlockPolicy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics
var isMandatory: Bool { appSettings.appLockIsMandatory }
@@ -38,21 +39,34 @@ class AppLockService: AppLockServiceProtocol {
}
}
var biometryType: LABiometryType { context.biometryType }
var biometricUnlockEnabled = false // Needs to be stored, not sure if in the keychain or defaults yet.
var biometryType: LABiometryType {
updateBiometrics()
guard context.evaluatedPolicyDomainState != nil else { return .none }
return context.biometryType
}
var biometricUnlockEnabled: Bool {
keychainController.containsPINCodeBiometricState()
}
var biometricUnlockTrusted: Bool {
guard let state = keychainController.pinCodeBiometricState() else { return false }
updateBiometrics()
return state == context.evaluatedPolicyDomainState
}
var numberOfPINAttempts: AnyPublisher<Int, Never> { appSettings.$appLockNumberOfPINAttempts }
var numberOfBiometricAttempts: AnyPublisher<Int, Never> { appSettings.$appLockNumberOfBiometricAttempts }
private var disabledSubject: PassthroughSubject<Void, Never> = .init()
var disabledPublisher: AnyPublisher<Void, Never> { disabledSubject.eraseToAnyPublisher() }
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings) {
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings, context: LAContext = .init()) {
self.keychainController = keychainController
self.appSettings = appSettings
self.context = context
timer = AppLockTimer(gracePeriod: appSettings.appLockGracePeriod)
configureBiometrics()
updateBiometrics()
}
func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError> {
@@ -74,11 +88,27 @@ class AppLockService: AppLockServiceProtocol {
return .success(())
}
func enableBiometricUnlock() -> Result<Void, AppLockServiceError> {
guard isEnabled else { return .failure(.pinNotSet) }
guard let state = context.evaluatedPolicyDomainState else { return .failure(.biometricUnlockNotSupported) }
do {
try keychainController.setPINCodeBiometricState(state)
return .success(())
} catch {
MXLog.error("Keychain access error: \(error)")
return .failure(.keychainError)
}
}
func disableBiometricUnlock() {
keychainController.removePINCodeBiometricState()
}
func disable() {
biometricUnlockEnabled = false
keychainController.removePINCode()
keychainController.removePINCodeBiometricState()
appSettings.appLockNumberOfPINAttempts = 0
appSettings.appLockNumberOfBiometricAttempts = 0
disabledSubject.send()
}
@@ -96,35 +126,68 @@ class AppLockService: AppLockServiceProtocol {
appSettings.appLockNumberOfPINAttempts += 1
return false
}
if biometricUnlockEnabled, !biometricUnlockTrusted {
MXLog.info("Fixing trust for biometric unlock.")
updateBiometrics()
_ = enableBiometricUnlock()
}
return completeUnlock()
}
func unlockWithBiometrics() -> Bool {
func unlockWithBiometrics() async -> Bool {
guard biometryType != .none, biometricUnlockEnabled else {
MXLog.warning("\(biometryType) failed.")
appSettings.appLockNumberOfBiometricAttempts += 1
MXLog.error("Biometric unlock not setup.")
return false
}
guard biometricUnlockTrusted else {
MXLog.error("Biometrics have changed. PIN should be shown.")
return false
}
do {
let context = unlockContext()
guard try await context.evaluatePolicy(unlockPolicy, localizedReason: L10n.screenAppLockBiometricUnlockReasonIos) else {
MXLog.warning("\(context.biometryType) failed without error.")
return false
}
return completeUnlock()
} catch {
MXLog.error("\(context.biometryType) failed: \(error)")
return false
}
return completeUnlock()
}
// MARK: - Private
/// Queries the context for supported biometrics.
private func configureBiometrics() {
/// Queries the context for supported biometrics and enrolment state.
private func updateBiometrics() {
var error: NSError?
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
context.canEvaluatePolicy(unlockPolicy, error: &error)
if let error {
MXLog.error("Biometrics error: \(error)")
}
}
/// Shared logic for completing an unlock via a PIN or biometry.
/// Creates a context specifically for unlocking the app. The titles are customised,
/// and the fresh context ensures that the user is promoted to unlock based on
/// `timer.gracePeriod` rather than any system to defined grace period.
private func unlockContext() -> LAContext {
// Keep using the injected context for tests etc.
guard type(of: context) == LAContext.self else { return context }
let context = LAContext()
context.localizedFallbackTitle = L10n.actionEnterPin
return context
}
/// Shared logic for completing an unlock via a PIN or biometrics.
private func completeUnlock() -> Bool {
timer.registerUnlock()
appSettings.appLockNumberOfPINAttempts = 0
appSettings.appLockNumberOfBiometricAttempts = 0
return true
}
}
@@ -24,6 +24,10 @@ enum AppLockServiceError: Error {
case invalidPIN
/// The PIN code was rejected as an insecure choice.
case weakPIN
/// A PIN code hasn't been set yet.
case pinNotSet
/// Attempting to use biometric unlock when it isn't yet supported on this device.
case biometricUnlockNotSupported
}
@MainActor
@@ -36,7 +40,10 @@ protocol AppLockServiceProtocol: AnyObject {
/// The type of biometric authentication supported by the device.
var biometryType: LABiometryType { get }
/// Whether or not the user has enabled unlock via TouchID, FaceID or (possibly) OpticID.
var biometricUnlockEnabled: Bool { get set }
var biometricUnlockEnabled: Bool { get }
/// Whether TouchID, FaceID or (possibly) OpticID are trusted, or if the app needs the user
/// to re-enter their PIN code to re-enable the feature (i.e. to accept a new face or fingerprint).
var biometricUnlockTrusted: Bool { get }
/// A publisher that advertises when the service has been disabled.
var disabledPublisher: AnyPublisher<Void, Never> { get }
@@ -45,6 +52,10 @@ protocol AppLockServiceProtocol: AnyObject {
func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError>
/// Validates the supplied PIN code is long enough, only contains digits and isn't a weak choice.
func validate(_ pinCode: String) -> Result<Void, AppLockServiceError>
/// Enables the use of Touch ID/Face ID as an alternative to the PIN code.
func enableBiometricUnlock() -> Result<Void, AppLockServiceError>
/// Disables the use of Touch ID/Face ID as an alternative to the PIN code.
func disableBiometricUnlock()
/// Disables the App Lock feature, removing the user's stored PIN code.
func disable()
@@ -56,12 +67,12 @@ protocol AppLockServiceProtocol: AnyObject {
/// Attempt to unlock the app with the supplied PIN code.
func unlock(with pinCode: String) -> Bool
/// Attempt to unlock the app using FaceID or TouchID.
func unlockWithBiometrics() -> Bool
func unlockWithBiometrics() async -> Bool
/// The number of attempts the user had made to unlock with a PIN code.
///
/// Note: We don't track the biometric attempts as LAContext does that automatically.
var numberOfPINAttempts: AnyPublisher<Int, Never> { get }
/// The number of attempts the user has made to unlock with Touch/Face ID.
var numberOfBiometricAttempts: AnyPublisher<Int, Never> { get }
}
// sourcery: AutoMockable
@@ -73,7 +84,6 @@ extension AppLockServiceMock {
mock.isEnabled = pinCode != nil
mock.isMandatory = isMandatory
mock.numberOfPINAttempts = PassthroughSubject<Int, Never>().eraseToAnyPublisher()
mock.numberOfBiometricAttempts = PassthroughSubject<Int, Never>().eraseToAnyPublisher()
mock.underlyingBiometryType = biometryType
mock.underlyingBiometricUnlockEnabled = biometryType != .none
mock.unlockWithClosure = { $0 == pinCode }
@@ -31,7 +31,7 @@ class AppLockTimer {
/// Creates a new timer.
/// - Parameter gracePeriod: The amount of time the app should remain unlocked for whilst backgrounded.
init(gracePeriod: TimeInterval) {
self.gracePeriod = 180
self.gracePeriod = gracePeriod
}
/// Signals to the timer to track how long the app will be backgrounded for.
@@ -38,7 +38,8 @@ class KeychainController: KeychainControllerProtocol {
private let mainKeychain: Keychain
private enum Key: String {
case pinCode
case appLockPINCode
case appLockBiometricState
}
init(service: KeychainControllerService, accessGroup: String) {
@@ -129,16 +130,16 @@ class KeychainController: KeychainControllerProtocol {
}
func containsPINCode() throws -> Bool {
try mainKeychain.contains(Key.pinCode.rawValue)
try mainKeychain.contains(Key.appLockPINCode.rawValue)
}
func setPINCode(_ pinCode: String) throws {
try mainKeychain.set(pinCode, key: Key.pinCode.rawValue)
try mainKeychain.set(pinCode, key: Key.appLockPINCode.rawValue)
}
func pinCode() -> String? {
do {
return try mainKeychain.getString(Key.pinCode.rawValue)
return try mainKeychain.getString(Key.appLockPINCode.rawValue)
} catch {
MXLog.error("Failed retrieving the PIN code.")
return nil
@@ -147,9 +148,39 @@ class KeychainController: KeychainControllerProtocol {
func removePINCode() {
do {
try mainKeychain.remove(Key.pinCode.rawValue)
try mainKeychain.remove(Key.appLockPINCode.rawValue)
} catch {
MXLog.error("Failed removing the PIN code.")
}
}
func containsPINCodeBiometricState() -> Bool {
do {
return try mainKeychain.contains(Key.appLockBiometricState.rawValue)
} catch {
MXLog.error("Failed checking for biometric state.")
return false // No need to re-throw the error, we can fall back to the PIN code.
}
}
func setPINCodeBiometricState(_ state: Data) throws {
try mainKeychain.set(state, key: Key.appLockBiometricState.rawValue)
}
func pinCodeBiometricState() -> Data? {
do {
return try mainKeychain.getData(Key.appLockBiometricState.rawValue)
} catch {
MXLog.error("Failed setting the PIN code biometric state.")
return nil
}
}
func removePINCodeBiometricState() {
do {
try mainKeychain.remove(Key.appLockBiometricState.rawValue)
} catch {
MXLog.error("Failed removing the PIN code biometric state.")
}
}
}
@@ -44,4 +44,12 @@ protocol KeychainControllerProtocol: ClientSessionDelegate {
func pinCode() -> String?
/// Removes the App Lock PIN code.
func removePINCode()
/// Whether or not PIN code biometric state has been set.
func containsPINCodeBiometricState() -> Bool
/// Sets the PIN code biometric state for App Lock.
func setPINCodeBiometricState(_ state: Data) throws
/// The PIN code biometric state required to use Touch/Face ID to unlock the app.
func pinCodeBiometricState() -> Data?
/// Removes the App Lock PIN code biometric state.
func removePINCodeBiometricState()
}