mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			274 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			274 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import LocalAuthentication
 | 
						|
 | 
						|
@objc public class OWSScreenLock: NSObject {
 | 
						|
 | 
						|
    public enum OWSScreenLockOutcome {
 | 
						|
        case success
 | 
						|
        case cancel
 | 
						|
        case failure(error:String)
 | 
						|
        case unexpectedFailure(error:String)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc public let screenLockTimeoutDefault = 15 * kMinuteInterval
 | 
						|
    @objc public let screenLockTimeouts = [
 | 
						|
        1 * kMinuteInterval,
 | 
						|
        5 * kMinuteInterval,
 | 
						|
        15 * kMinuteInterval,
 | 
						|
        30 * kMinuteInterval,
 | 
						|
        1 * kHourInterval,
 | 
						|
        0
 | 
						|
    ]
 | 
						|
 | 
						|
    @objc public static let ScreenLockDidChange = Notification.Name("ScreenLockDidChange")
 | 
						|
 | 
						|
    let primaryStorage: OWSPrimaryStorage
 | 
						|
    let dbConnection: YapDatabaseConnection
 | 
						|
 | 
						|
    private let OWSScreenLock_Collection = "OWSScreenLock_Collection"
 | 
						|
    private let OWSScreenLock_Key_IsScreenLockEnabled = "OWSScreenLock_Key_IsScreenLockEnabled"
 | 
						|
    private let OWSScreenLock_Key_ScreenLockTimeoutSeconds = "OWSScreenLock_Key_ScreenLockTimeoutSeconds"
 | 
						|
 | 
						|
    // MARK: - Singleton class
 | 
						|
 | 
						|
    @objc(sharedManager)
 | 
						|
    public static let shared = OWSScreenLock()
 | 
						|
 | 
						|
    private override init() {
 | 
						|
        self.primaryStorage = OWSPrimaryStorage.shared()
 | 
						|
        self.dbConnection = self.primaryStorage.newDatabaseConnection()
 | 
						|
 | 
						|
        super.init()
 | 
						|
 | 
						|
        SwiftSingletons.register(self)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Properties
 | 
						|
 | 
						|
    @objc public func isScreenLockEnabled() -> Bool {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
 | 
						|
        if !OWSStorage.isStorageReady() {
 | 
						|
            owsFailDebug("accessed screen lock state before storage is ready.")
 | 
						|
            return false
 | 
						|
        }
 | 
						|
 | 
						|
        return self.dbConnection.bool(forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection, defaultValue: false)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func setIsScreenLockEnabled(_ value: Bool) {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
        assert(OWSStorage.isStorageReady())
 | 
						|
 | 
						|
        self.dbConnection.setBool(value, forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection)
 | 
						|
 | 
						|
        NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc public func screenLockTimeout() -> TimeInterval {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
 | 
						|
        if !OWSStorage.isStorageReady() {
 | 
						|
            owsFailDebug("accessed screen lock state before storage is ready.")
 | 
						|
            return 0
 | 
						|
        }
 | 
						|
 | 
						|
        return self.dbConnection.double(forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection, defaultValue: screenLockTimeoutDefault)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc public func setScreenLockTimeout(_ value: TimeInterval) {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
        assert(OWSStorage.isStorageReady())
 | 
						|
 | 
						|
        self.dbConnection.setDouble(value, forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection)
 | 
						|
 | 
						|
        NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Methods
 | 
						|
 | 
						|
    // This method should only be called:
 | 
						|
    //
 | 
						|
    // * On the main thread.
 | 
						|
    //
 | 
						|
    // Exactly one of these completions will be performed:
 | 
						|
    //
 | 
						|
    // * Asynchronously.
 | 
						|
    // * On the main thread.
 | 
						|
    @objc public func tryToUnlockScreenLock(success: @escaping (() -> Void),
 | 
						|
                                            failure: @escaping ((Error) -> Void),
 | 
						|
                                            unexpectedFailure: @escaping ((Error) -> Void),
 | 
						|
                                            cancel: @escaping (() -> Void)) {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
 | 
						|
        tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK",
 | 
						|
                                                                          comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'."),
 | 
						|
                                       completion: { (outcome: OWSScreenLockOutcome) in
 | 
						|
                                        AssertIsOnMainThread()
 | 
						|
 | 
						|
                                        switch outcome {
 | 
						|
                                        case .failure(let error):
 | 
						|
                                            Logger.error("local authentication failed with error: \(error)")
 | 
						|
                                            failure(self.authenticationError(errorDescription: error))
 | 
						|
                                        case .unexpectedFailure(let error):
 | 
						|
                                            Logger.error("local authentication failed with unexpected error: \(error)")
 | 
						|
                                            unexpectedFailure(self.authenticationError(errorDescription: error))
 | 
						|
                                        case .success:
 | 
						|
                                            Logger.verbose("local authentication succeeded.")
 | 
						|
                                            success()
 | 
						|
                                        case .cancel:
 | 
						|
                                            Logger.verbose("local authentication cancelled.")
 | 
						|
                                            cancel()
 | 
						|
                                        }
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    // This method should only be called:
 | 
						|
    //
 | 
						|
    // * On the main thread.
 | 
						|
    //
 | 
						|
    // completionParam will be performed:
 | 
						|
    //
 | 
						|
    // * Asynchronously.
 | 
						|
    // * On the main thread.
 | 
						|
    private func tryToVerifyLocalAuthentication(localizedReason: String,
 | 
						|
                                                completion completionParam: @escaping ((OWSScreenLockOutcome) -> Void)) {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
 | 
						|
        let defaultErrorDescription = NSLocalizedString("SCREEN_LOCK_ENABLE_UNKNOWN_ERROR",
 | 
						|
                                                        comment: "Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode.")
 | 
						|
 | 
						|
        // Ensure completion is always called on the main thread.
 | 
						|
        let completion = { (outcome: OWSScreenLockOutcome) in
 | 
						|
            DispatchQueue.main.async {
 | 
						|
                completionParam(outcome)
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        let context = screenLockContext()
 | 
						|
 | 
						|
        var authError: NSError?
 | 
						|
        let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authError)
 | 
						|
        if !canEvaluatePolicy || authError != nil {
 | 
						|
            Logger.error("could not determine if local authentication is supported: \(String(describing: authError))")
 | 
						|
 | 
						|
            let outcome = self.outcomeForLAError(errorParam: authError,
 | 
						|
                                                 defaultErrorDescription: defaultErrorDescription)
 | 
						|
            switch outcome {
 | 
						|
            case .success:
 | 
						|
                owsFailDebug("local authentication unexpected success")
 | 
						|
                completion(.failure(error:defaultErrorDescription))
 | 
						|
            case .cancel, .failure, .unexpectedFailure:
 | 
						|
                completion(outcome)
 | 
						|
            }
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: localizedReason) { success, evaluateError in
 | 
						|
 | 
						|
            if success {
 | 
						|
                Logger.info("local authentication succeeded.")
 | 
						|
                completion(.success)
 | 
						|
            } else {
 | 
						|
                let outcome = self.outcomeForLAError(errorParam: evaluateError,
 | 
						|
                                                     defaultErrorDescription: defaultErrorDescription)
 | 
						|
                switch outcome {
 | 
						|
                case .success:
 | 
						|
                    owsFailDebug("local authentication unexpected success")
 | 
						|
                    completion(.failure(error:defaultErrorDescription))
 | 
						|
                case .cancel, .failure, .unexpectedFailure:
 | 
						|
                    completion(outcome)
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Outcome
 | 
						|
 | 
						|
    private func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> OWSScreenLockOutcome {
 | 
						|
        if let error = errorParam {
 | 
						|
            guard let laError = error as? LAError else {
 | 
						|
                return .failure(error:defaultErrorDescription)
 | 
						|
            }
 | 
						|
 | 
						|
            if #available(iOS 11.0, *) {
 | 
						|
                switch laError.code {
 | 
						|
                case .biometryNotAvailable:
 | 
						|
                    Logger.error("local authentication error: biometryNotAvailable.")
 | 
						|
                    return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
 | 
						|
                                                             comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device."))
 | 
						|
                case .biometryNotEnrolled:
 | 
						|
                    Logger.error("local authentication error: biometryNotEnrolled.")
 | 
						|
                    return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
 | 
						|
                                                             comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device."))
 | 
						|
                case .biometryLockout:
 | 
						|
                    Logger.error("local authentication error: biometryLockout.")
 | 
						|
                    return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
 | 
						|
                                                             comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures."))
 | 
						|
                default:
 | 
						|
                    // Fall through to second switch
 | 
						|
                    break
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            switch laError.code {
 | 
						|
            case .authenticationFailed:
 | 
						|
                Logger.error("local authentication error: authenticationFailed.")
 | 
						|
                return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED",
 | 
						|
                                                         comment: "Indicates that Touch ID/Face ID/Phone Passcode authentication failed."))
 | 
						|
            case .userCancel, .userFallback, .systemCancel, .appCancel:
 | 
						|
                Logger.info("local authentication cancelled.")
 | 
						|
                return .cancel
 | 
						|
            case .passcodeNotSet:
 | 
						|
                Logger.error("local authentication error: passcodeNotSet.")
 | 
						|
                return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET",
 | 
						|
                                                         comment: "Indicates that Touch ID/Face ID/Phone Passcode passcode is not set."))
 | 
						|
            case .touchIDNotAvailable:
 | 
						|
                Logger.error("local authentication error: touchIDNotAvailable.")
 | 
						|
                return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
 | 
						|
                                                         comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device."))
 | 
						|
            case .touchIDNotEnrolled:
 | 
						|
                Logger.error("local authentication error: touchIDNotEnrolled.")
 | 
						|
                return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
 | 
						|
                                                         comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device."))
 | 
						|
            case .touchIDLockout:
 | 
						|
                Logger.error("local authentication error: touchIDLockout.")
 | 
						|
                return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
 | 
						|
                                                         comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures."))
 | 
						|
            case .invalidContext:
 | 
						|
                owsFailDebug("context not valid.")
 | 
						|
                return .unexpectedFailure(error:defaultErrorDescription)
 | 
						|
            case .notInteractive:
 | 
						|
                owsFailDebug("context not interactive.")
 | 
						|
                return .unexpectedFailure(error:defaultErrorDescription)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return .failure(error:defaultErrorDescription)
 | 
						|
    }
 | 
						|
 | 
						|
    private func authenticationError(errorDescription: String) -> Error {
 | 
						|
        return OWSErrorWithCodeDescription(.localAuthenticationError,
 | 
						|
                                           errorDescription)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Context
 | 
						|
 | 
						|
    private func screenLockContext() -> LAContext {
 | 
						|
        let context = LAContext()
 | 
						|
 | 
						|
        // Never recycle biometric auth.
 | 
						|
        context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(0)
 | 
						|
 | 
						|
        if #available(iOS 11.0, *) {
 | 
						|
            assert(!context.interactionNotAllowed)
 | 
						|
        }
 | 
						|
 | 
						|
        return context
 | 
						|
    }
 | 
						|
}
 |