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
 | |
|     }
 | |
| }
 |