KeychainCoordinator.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /*****************************************************************************
  2. * KeychainCoordinator.m
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2017 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors:Carola Nitz <caro # videolan.org>
  9. *
  10. * Refer to the COPYING file of the official project for license.
  11. *****************************************************************************/
  12. import Foundation
  13. import LocalAuthentication
  14. @objc(VLCKeychainCoordinator)
  15. class KeychainCoordinator:NSObject, PAPasscodeViewControllerDelegate {
  16. @objc class var passcodeLockEnabled:Bool {
  17. return UserDefaults.standard.bool(forKey:kVLCSettingPasscodeOnKey)
  18. }
  19. private var laContext = LAContext()
  20. //Since FaceID and TouchID are both set to 1 when the defaults are registered
  21. //we have to double check for the biometry type to not return true even though the setting is not visible
  22. //and that type is not supported by the device
  23. private var touchIDEnabled:Bool {
  24. var touchIDEnabled = UserDefaults.standard.bool(forKey:kVLCSettingPasscodeAllowTouchID)
  25. if #available(iOS 11.0, *), laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
  26. touchIDEnabled = touchIDEnabled && laContext.biometryType == .touchID
  27. }
  28. return touchIDEnabled
  29. }
  30. private var faceIDEnabled:Bool {
  31. var faceIDEnabled = UserDefaults.standard.bool(forKey:kVLCSettingPasscodeAllowFaceID)
  32. if #available(iOS 11.0, *), laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
  33. faceIDEnabled = faceIDEnabled && laContext.biometryType == .faceID
  34. }
  35. return faceIDEnabled
  36. }
  37. static let passcodeService = "org.videolan.vlc-ios.passcode"
  38. var completion: (() -> ())? = nil
  39. private var avoidPromptingTouchOrFaceID = false
  40. private lazy var passcodeLockController:PAPasscodeViewController = {
  41. let passcodeController = PAPasscodeViewController(for: PasscodeActionEnter)
  42. passcodeController!.delegate = self
  43. return passcodeController!
  44. }()
  45. override init() {
  46. super.init()
  47. NotificationCenter.default.addObserver(self, selector: #selector(appInForeground), name: .UIApplicationDidBecomeActive, object: nil)
  48. }
  49. @objc class func setPasscode(passcode:String?) throws {
  50. guard let passcode = passcode else {
  51. do {
  52. try XKKeychainGenericPasswordItem.removeItems(forService: passcodeService)
  53. } catch let error {
  54. throw error
  55. }
  56. return
  57. }
  58. let keychainItem = XKKeychainGenericPasswordItem()
  59. keychainItem.service = passcodeService
  60. keychainItem.account = passcodeService
  61. keychainItem.secret.stringValue = passcode
  62. do {
  63. try keychainItem.save()
  64. } catch let error {
  65. throw error
  66. }
  67. }
  68. @objc func validatePasscode(completion:@escaping ()->()) {
  69. passcodeLockController.passcode = passcodeFromKeychain()
  70. self.completion = completion
  71. guard let rootViewController = UIApplication.shared.delegate?.window??.rootViewController else {
  72. return
  73. }
  74. if rootViewController.presentedViewController != nil {
  75. rootViewController.dismiss(animated: false, completion: nil)
  76. }
  77. let navigationController = UINavigationController(rootViewController: passcodeLockController)
  78. navigationController.modalPresentationStyle = .fullScreen
  79. navigationController.modalTransitionStyle = .crossDissolve
  80. rootViewController.present(navigationController, animated: true) {
  81. [weak self] in
  82. if (self?.touchIDEnabled == true || self?.faceIDEnabled == true) {
  83. self?.touchOrFaceIDQuery()
  84. }
  85. }
  86. }
  87. @objc private func appInForeground(notification:Notification) {
  88. if let navigationController = UIApplication.shared.delegate?.window??.rootViewController?.presentedViewController as? UINavigationController, navigationController.topViewController is PAPasscodeViewController,
  89. touchIDEnabled || faceIDEnabled {
  90. touchOrFaceIDQuery()
  91. }
  92. }
  93. private func touchOrFaceIDQuery() {
  94. if (avoidPromptingTouchOrFaceID || UIApplication.shared.applicationState != .active) {
  95. return
  96. }
  97. if laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil){
  98. avoidPromptingTouchOrFaceID = true
  99. laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
  100. localizedReason: NSLocalizedString("BIOMETRIC_UNLOCK", comment: ""),
  101. reply: { [weak self ] success, _ in
  102. DispatchQueue.main.async {
  103. if success {
  104. UIApplication.shared.delegate?.window??.rootViewController?.dismiss(animated: true, completion: {
  105. self?.completion?()
  106. self?.completion = nil
  107. self?.avoidPromptingTouchOrFaceID = false
  108. })
  109. } else {
  110. //user hit cancel and wants to enter the passcode
  111. self?.avoidPromptingTouchOrFaceID = true
  112. }
  113. }
  114. })
  115. }
  116. }
  117. private func passcodeFromKeychain() -> String {
  118. if let item = try? XKKeychainGenericPasswordItem(forService: KeychainCoordinator.passcodeService, account: KeychainCoordinator.passcodeService) {
  119. return item.secret.stringValue
  120. }
  121. assert(false, "Couldn't retrieve item from Keychain! If passcodeLockEnabled we should have an item and secret")
  122. return ""
  123. }
  124. //MARK: PAPassCodeDelegate
  125. func paPasscodeViewControllerDidEnterPasscode(_ controller: PAPasscodeViewController!) {
  126. avoidPromptingTouchOrFaceID = false
  127. UIApplication.shared.delegate?.window??.rootViewController?.dismiss(animated: true, completion: {
  128. self.completion?()
  129. self.completion = nil
  130. })
  131. }
  132. }