KeychainCoordinator.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. // MARK: - PAPasscodeViewController UI
  15. extension PAPasscodeViewController {
  16. open override var preferredStatusBarStyle: UIStatusBarStyle {
  17. return PresentationTheme.current.colors.statusBarStyle
  18. }
  19. }
  20. @objc(VLCKeychainCoordinator)
  21. class KeychainCoordinator: NSObject, PAPasscodeViewControllerDelegate {
  22. @objc class var passcodeLockEnabled: Bool {
  23. return UserDefaults.standard.bool(forKey: kVLCSettingPasscodeOnKey)
  24. }
  25. // Since FaceID and TouchID are both set to 1 when the defaults are registered
  26. // we have to double check for the biometry type to not return true even though the setting is not visible
  27. // and that type is not supported by the device
  28. private var touchIDEnabled: Bool {
  29. var touchIDEnabled = UserDefaults.standard.bool(forKey: kVLCSettingPasscodeAllowTouchID)
  30. let laContext = LAContext()
  31. if #available(iOS 11.0.1, *), laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
  32. touchIDEnabled = touchIDEnabled && laContext.biometryType == .touchID
  33. }
  34. return touchIDEnabled
  35. }
  36. private var faceIDEnabled: Bool {
  37. var faceIDEnabled = UserDefaults.standard.bool(forKey: kVLCSettingPasscodeAllowFaceID)
  38. let laContext = LAContext()
  39. if #available(iOS 11.0.1, *), laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
  40. faceIDEnabled = faceIDEnabled && laContext.biometryType == .faceID
  41. }
  42. return faceIDEnabled
  43. }
  44. static let passcodeService = "org.videolan.vlc-ios.passcode"
  45. var completion: (() -> Void)?
  46. private var avoidPromptingTouchOrFaceID = false
  47. private lazy var passcodeLockController: PAPasscodeViewController = {
  48. let passcodeController = PAPasscodeViewController(for: PasscodeActionEnter)
  49. passcodeController!.delegate = self
  50. return passcodeController!
  51. }()
  52. override init() {
  53. super.init()
  54. NotificationCenter.default.addObserver(self, selector: #selector(appInForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
  55. }
  56. @objc class func setPasscode(passcode: String?) throws {
  57. guard let passcode = passcode else {
  58. do {
  59. try XKKeychainGenericPasswordItem.removeItems(forService: passcodeService)
  60. } catch let error {
  61. throw error
  62. }
  63. return
  64. }
  65. let keychainItem = XKKeychainGenericPasswordItem()
  66. keychainItem.service = passcodeService
  67. keychainItem.account = passcodeService
  68. keychainItem.secret.stringValue = passcode
  69. do {
  70. try keychainItem.save()
  71. } catch let error {
  72. throw error
  73. }
  74. }
  75. @objc func validatePasscode(completion: @escaping () -> Void) {
  76. passcodeLockController.passcode = passcodeFromKeychain()
  77. self.completion = completion
  78. guard let rootViewController = UIApplication.shared.delegate?.window??.rootViewController, passcodeLockController.passcode != "" else {
  79. self.completion?()
  80. self.completion = nil
  81. return
  82. }
  83. //if we have no video displayed we should use the current rootViewController
  84. var presentingViewController = rootViewController
  85. if let presentedViewController = rootViewController.presentedViewController {
  86. //but if a video is playing we have the MovieViewController presented and want to show it above
  87. presentingViewController = presentedViewController
  88. }
  89. let navigationController = UINavigationController(rootViewController: passcodeLockController)
  90. navigationController.modalPresentationStyle = .fullScreen
  91. navigationController.modalTransitionStyle = .crossDissolve
  92. presentingViewController.present(navigationController, animated: true) {
  93. [weak self] in
  94. if self?.touchIDEnabled == true || self?.faceIDEnabled == true {
  95. self?.touchOrFaceIDQuery()
  96. }
  97. }
  98. }
  99. @objc private func appInForeground(notification: Notification) {
  100. if let navigationController = UIApplication.shared.delegate?.window??.rootViewController?.presentedViewController as? UINavigationController, navigationController.topViewController is PAPasscodeViewController,
  101. touchIDEnabled || faceIDEnabled {
  102. touchOrFaceIDQuery()
  103. }
  104. }
  105. private func touchOrFaceIDQuery() {
  106. if avoidPromptingTouchOrFaceID || UIApplication.shared.applicationState != .active {
  107. return
  108. }
  109. let laContext = LAContext()
  110. if laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
  111. avoidPromptingTouchOrFaceID = true
  112. laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
  113. localizedReason: NSLocalizedString("BIOMETRIC_UNLOCK", comment: ""),
  114. reply: { [weak self] success, _ in
  115. DispatchQueue.main.async {
  116. if success {
  117. UIApplication.shared.delegate?.window??.rootViewController?.dismiss(animated: true, completion: {
  118. self?.completion?()
  119. self?.completion = nil
  120. self?.avoidPromptingTouchOrFaceID = false
  121. })
  122. } else {
  123. // user hit cancel and wants to enter the passcode
  124. self?.avoidPromptingTouchOrFaceID = true
  125. }
  126. }
  127. })
  128. }
  129. }
  130. private func passcodeFromKeychain() -> String {
  131. do {
  132. let item = try XKKeychainGenericPasswordItem(forService: KeychainCoordinator.passcodeService, account: KeychainCoordinator.passcodeService)
  133. return item.secret.stringValue
  134. } catch let error {
  135. assert(false, "Couldn't retrieve item from Keychain! If passcodeLockEnabled we should have an item and secret. Error was \(error)")
  136. return ""
  137. }
  138. }
  139. // MARK: PAPassCodeDelegate
  140. func paPasscodeViewControllerDidEnterPasscode(_ controller: PAPasscodeViewController!) {
  141. avoidPromptingTouchOrFaceID = false
  142. if let navigationController = UIApplication.shared.delegate?.window??.rootViewController?.presentedViewController as? UINavigationController,
  143. let passcodeController = navigationController.topViewController?.presentedViewController as? PAPasscodeViewController ??
  144. navigationController.topViewController {
  145. //either dismiss the papasscode presented from movieVC or as topViewController
  146. passcodeController.dismiss(animated: true, completion: {
  147. [weak self] in
  148. self?.completion?()
  149. self?.completion = nil
  150. })
  151. }
  152. }
  153. }