/* Copyright 2018-2024 New Vector Ltd. 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 /// 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 `KeychainStore` private let store: KeyValueStore override private init() { store = KeychainStore(withKeychain: Keychain(service: PinConstants.pinCodeKeychainService, accessGroup: BuildSettings.keychainAccessGroup)) super.init() } // MARK: - Public /// Setting to force protection by pin code var forcePinProtection: Bool { return BuildSettings.forcePinProtection } /// Not allowed pin codes. User won't be able to select one of the pin in the list. var notAllowedPINs: [String] { return BuildSettings.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 { 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 } return result } /// 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 BuildSettings.pinCodeGraceTimeInSeconds } /// Number of digits for the PIN let numberOfDigits: Int = 4 /// Is user has set a pin var isPinSet: Bool { return pin != nil } /// Saved user PIN var pin: String? { get { do { return try store.string(forKey: StoreKeys.pin) } catch let error { MXLog.debug("[PinCodePreferences] Error when reading user pin from store: \(error)") return nil } } set { do { try store.set(newValue, forKey: StoreKeys.pin) } catch let error { MXLog.debug("[PinCodePreferences] Error when storing user pin to the store: \(error)") } } } var biometricsEnabled: Bool? { get { do { return try store.bool(forKey: StoreKeys.biometricsEnabled) } catch let error { MXLog.debug("[PinCodePreferences] Error when reading biometrics enabled from store: \(error)") return nil } } set { do { try store.set(newValue, forKey: StoreKeys.biometricsEnabled) } catch let error { MXLog.debug("[PinCodePreferences] Error when storing biometrics enabled to the store: \(error)") } } } var canUseBiometricsToUnlock: Bool? { get { guard isBiometricsAvailable == true else { return false } do { return try store.bool(forKey: StoreKeys.canUseBiometricsToUnlock) } catch let error { MXLog.debug("[PinCodePreferences] Error when reading canUseBiometricsToUnlock from store: \(error)") return nil } } set { do { try store.set(newValue, forKey: StoreKeys.canUseBiometricsToUnlock) } catch let error { MXLog.debug("[PinCodePreferences] Error when storing canUseBiometricsToUnlock to the store: \(error)") } } } var numberOfPinFailures: Int { get { do { return try store.integer(forKey: StoreKeys.numberOfPinFailures) ?? 0 } catch let error { MXLog.debug("[PinCodePreferences] Error when reading numberOfPinFailures from store: \(error)") return 0 } } set { do { try store.set(newValue, forKey: StoreKeys.numberOfPinFailures) } catch let error { MXLog.debug("[PinCodePreferences] Error when storing numberOfPinFailures to the store: \(error)") } } } var numberOfBiometricsFailures: Int { get { do { return try store.integer(forKey: StoreKeys.numberOfBiometricsFailures) ?? 0 } catch let error { MXLog.debug("[PinCodePreferences] Error when reading numberOfBiometricsFailures from store: \(error)") return 0 } } set { do { try store.set(newValue, forKey: StoreKeys.numberOfBiometricsFailures) } catch let error { MXLog.debug("[PinCodePreferences] Error when storing numberOfBiometricsFailures to the store: \(error)") } } } var isBiometricsSet: Bool { return biometricsEnabled == true && (canUseBiometricsToUnlock ?? true) } 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 = nil canUseBiometricsToUnlock = nil resetCounters() } /// Reset number of failures for both pin and biometrics func resetCounters() { numberOfPinFailures = 0 numberOfBiometricsFailures = 0 } }