/* Copyright 2018-2024 New Vector Ltd. Copyright (c) 2021 BWI GmbH SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ import Foundation import KeychainAccess import LocalAuthentication import MatrixSDK extension Notification.Name { /// Posted when fallbacking to PIN from a biometrics protection static let biometricsDidFallbackToPin = Notification.Name("BiometricsDidFallbackToPin") } /// Pin code preferences. @objcMembers final class PinCodePreferences: NSObject { // MARK: - Constants private struct PinConstants { static let pinCodeKeychainService: String = BuildSettings.baseBundleIdentifier + ".pin-service" } private struct StoreKeys { static let pin: String = "pin" static let biometricsEnabled: String = "biometricsEnabled" static let canUseBiometricsToUnlock: String = "canUseBiometricsToUnlock" static let numberOfPinFailures: String = "numberOfPinFailures" static let numberOfBiometricsFailures: String = "numberOfBiometricsFailures" } static let shared = PinCodePreferences() /// Store. Defaults to `SecureFileStore` private let secureStore: SecureFileStorage override private init() { secureStore = SecureFileStorage.shared super.init() migrateOldData() } private func migrateOldData() { let oldStore = KeychainVault(Keychain(service: PinConstants.pinCodeKeychainService, accessGroup: BuildSettings.keychainAccessGroup)) if let oldPin = try? oldStore.string(forKey: StoreKeys.pin) { do { // migrate pin = oldPin // clear old pin try oldStore.removeObject(forKey: StoreKeys.pin) } catch let error { MXLog.error("[PinCodePreferences] Error when migrating old user pin:") } } if let oldBiometricsEnabled = try? oldStore.bool(forKey: StoreKeys.biometricsEnabled) { biometricsEnabled = oldBiometricsEnabled try? oldStore.removeObject(forKey: StoreKeys.biometricsEnabled) } if let oldCanUseBiometricsToUnlock = try? oldStore.bool(forKey: StoreKeys.canUseBiometricsToUnlock) { canUseBiometricsToUnlock = oldCanUseBiometricsToUnlock try? oldStore.removeObject(forKey: StoreKeys.canUseBiometricsToUnlock) } if let oldNumberOfPinFailures = try? oldStore.integer(forKey: StoreKeys.numberOfPinFailures) { numberOfPinFailures = oldNumberOfPinFailures try? oldStore.removeObject(forKey: StoreKeys.numberOfPinFailures) } if let oldNumberOfBiometricsFailures = try? oldStore.integer(forKey: StoreKeys.numberOfBiometricsFailures) { numberOfBiometricsFailures = oldNumberOfBiometricsFailures try? oldStore.removeObject(forKey: StoreKeys.numberOfBiometricsFailures) } } // MARK: - Public /// Setting to force protection by pin code var forcePinProtection: Bool { return BWIBuildSettings.shared.forcePinProtection } /// Not allowed pin codes. User won't be able to select one of the pin in the list. var notAllowedPINs: [String] { return BWIBuildSettings.shared.notAllowedPINs } /// Maximum number of allowed pin failures when unlocking, before force logging out the user var maxAllowedNumberOfPinFailures: Int { return BuildSettings.maxAllowedNumberOfPinFailures } /// Maximum number of allowed biometrics failures when unlocking, before fallbacking the user to the pin var maxAllowedNumberOfBiometricsFailures: Int { return BuildSettings.maxAllowedNumberOfBiometricsFailures } var isBiometricsAvailable: Bool { let context = LAContext() var error: NSError? let result = LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) // While in lockout they're still techincally available if error?.code == LAError.Code.biometryLockout.rawValue { return true } if result == true { return context.biometryType == .touchID || context.biometryType == .faceID } return false } /// Allowed number of PIN trials before showing forgot help alert let allowedNumberOfTrialsBeforeAlert: Int = 5 /// Max allowed time to continue using the app without prompting PIN var graceTimeInSeconds: TimeInterval { return BWIBuildSettings.shared.pinCodeGraceTimeInSeconds } /// Number of digits for the PIN let numberOfDigits: Int = 4 /// Is user has set a pin var isPinSet: Bool { return secureStore.objectExists(withKey: StoreKeys.pin) } /// Saved user PIN var pin: String? { get { do { return try secureStore.string(forKey: StoreKeys.pin) } catch let error { MXLog.debug("[PinCodePreferences] Error when reading user pin from store: \(error)") return nil } } set { do { if let newPin = newValue { if !secureStore.locked { try secureStore.update(passphrase: newPin) } else { try secureStore.unlock(passphrase: newPin) } } try secureStore.set(newValue, forKey: StoreKeys.pin) } catch let error { MXLog.debug("[PinCodePreferences] Error when storing user pin to the store: \(error)") } } } func pinsMatch(_ pinCode: String) -> Bool { do { try secureStore.unlock(passphrase: pinCode) let pin = try secureStore.string(forKey: StoreKeys.pin) return pinCode == pin } catch let error { MXLog.error("[PinCodePreferences] Error when checking user pin") return false } } var biometricsEnabled: Bool { get { return RiotSettings.shared.biometricsEnabled } set { RiotSettings.shared.biometricsEnabled = newValue } } var canUseBiometricsToUnlock: Bool? { get { guard isBiometricsAvailable == true else { return false } do { return try secureStore.bool(forKey: StoreKeys.canUseBiometricsToUnlock) } catch { MXLog.debug("[PinCodePreferences] Error when reading canUseBiometricsToUnlock from store") return nil } } set { do { try secureStore.set(newValue, forKey: StoreKeys.canUseBiometricsToUnlock) } catch { MXLog.debug("[PinCodePreferences] Error when storing canUseBiometricsToUnlock to the store") } } } var numberOfPinFailures: Int { get { return RiotSettings.shared.numberOfPinFailures } set { RiotSettings.shared.numberOfPinFailures = newValue } } var numberOfBiometricsFailures: Int { get { return RiotSettings.shared.numberOfBiometricsFailures } set { RiotSettings.shared.numberOfBiometricsFailures = newValue } } var isBiometricsSet: Bool { return biometricsEnabled && (canUseBiometricsToUnlock ?? true) && isBiometricsAvailable } func localizedBiometricsName() -> String? { if isBiometricsAvailable { let context = LAContext() // canEvaluatePolicy should be called for biometryType to be set _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) switch context.biometryType { case .touchID: return VectorL10n.biometricsModeTouchId case .faceID: return VectorL10n.biometricsModeFaceId default: return nil } } return nil } func biometricsIcon() -> UIImage? { if isBiometricsAvailable { let context = LAContext() // canEvaluatePolicy should be called for biometryType to be set _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) switch context.biometryType { case .touchID: return Asset.Images.touchidIcon.image case .faceID: return Asset.Images.faceidIcon.image default: return nil } } return nil } /// Resets user PIN func reset() { pin = nil biometricsEnabled = false canUseBiometricsToUnlock = true resetCounters() } /// Reset number of failures for both pin and biometrics func resetCounters() { numberOfPinFailures = 0 numberOfBiometricsFailures = 0 } }