242 lines
7.8 KiB
Swift
242 lines
7.8 KiB
Swift
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import Foundation
|
|
import AudioToolbox
|
|
import LocalAuthentication
|
|
|
|
fileprivate struct Constants {
|
|
static let pinAttemptsNumber = 10
|
|
}
|
|
|
|
protocol PinAuthorizationCoordinatorDelegate: class {
|
|
func pinAuthorizationCoordinatorDidLogout(_ coordinator: PinAuthorizationCoordinator)
|
|
}
|
|
|
|
class PinAuthorizationCoordinator: NSObject {
|
|
weak var delegate: PinAuthorizationCoordinatorDelegate?
|
|
|
|
fileprivate enum State {
|
|
case unlocked
|
|
case locked(lockTime: CFTimeInterval)
|
|
case validatingPin
|
|
}
|
|
|
|
fileprivate let theme: Theme
|
|
fileprivate let window: UIWindow
|
|
|
|
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
|
|
|
|
fileprivate var state: State
|
|
|
|
var preventFromLocking: Bool = false {
|
|
didSet {
|
|
if !preventFromLocking && UIApplication.shared.applicationState != .active {
|
|
// In case if locking option change in background we want to lock app when user comes back.
|
|
lockIfNeeded(CACurrentMediaTime())
|
|
}
|
|
}
|
|
}
|
|
|
|
init(theme: Theme, submanagerObjects: OCTSubmanagerObjects, lockOnStartup: Bool) {
|
|
self.theme = theme
|
|
self.window = UIWindow(frame: UIScreen.main.bounds)
|
|
self.submanagerObjects = submanagerObjects
|
|
self.state = .unlocked
|
|
|
|
super.init()
|
|
|
|
// Showing window on top of all other windows.
|
|
window.windowLevel = UIWindowLevelStatusBar + 1000
|
|
|
|
if lockOnStartup {
|
|
lockIfNeeded(0)
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(PinAuthorizationCoordinator.appWillResignActiveNotification),
|
|
name: NSNotification.Name.UIApplicationWillResignActive,
|
|
object: nil)
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(PinAuthorizationCoordinator.appDidBecomeActiveNotification),
|
|
name: NSNotification.Name.UIApplicationDidBecomeActive,
|
|
object: nil)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
@objc func appWillResignActiveNotification() {
|
|
lockIfNeeded(CACurrentMediaTime())
|
|
}
|
|
|
|
@objc func appDidBecomeActiveNotification() {
|
|
switch state {
|
|
case .unlocked:
|
|
// unlocked, nothing to do here
|
|
break
|
|
case .locked(let lockTime):
|
|
isPinDateExpired(lockTime) ? challengeUserToAuthorize(lockTime) : unlock()
|
|
case .validatingPin:
|
|
// checking pin, no action required
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PinAuthorizationCoordinator: CoordinatorProtocol {
|
|
func startWithOptions(_ options: CoordinatorOptions?) {
|
|
switch state {
|
|
case .locked(let lockTime):
|
|
challengeUserToAuthorize(lockTime)
|
|
case .unlocked:
|
|
// ignore
|
|
break
|
|
case .validatingPin:
|
|
// ignore
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PinAuthorizationCoordinator: EnterPinControllerDelegate {
|
|
func enterPinController(_ controller: EnterPinController, successWithPin pin: String) {
|
|
unlock()
|
|
}
|
|
|
|
func enterPinControllerFailure(_ controller: EnterPinController) {
|
|
let keychainManager = KeychainManager()
|
|
|
|
var failedAttempts = keychainManager.failedPinAttemptsNumber ?? 0
|
|
failedAttempts += 1
|
|
|
|
keychainManager.failedPinAttemptsNumber = failedAttempts
|
|
|
|
guard failedAttempts < Constants.pinAttemptsNumber else {
|
|
keychainManager.failedPinAttemptsNumber = nil
|
|
handleErrorWithType(.pinLogOut)
|
|
|
|
delegate?.pinAuthorizationCoordinatorDidLogout(self)
|
|
return
|
|
}
|
|
|
|
controller.resetEnteredPin()
|
|
controller.topText = String(localized: "pin_incorrect")
|
|
controller.descriptionText = String(localized: "pin_failed_attempts", "\(failedAttempts)")
|
|
AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
|
|
}
|
|
}
|
|
|
|
private extension PinAuthorizationCoordinator {
|
|
func lockIfNeeded(_ lockTime: CFTimeInterval) {
|
|
guard submanagerObjects.getProfileSettings().unlockPinCode != nil else {
|
|
return
|
|
}
|
|
|
|
if preventFromLocking {
|
|
return
|
|
}
|
|
|
|
for window in UIApplication.shared.windows {
|
|
window.endEditing(true)
|
|
}
|
|
|
|
let storyboard = UIStoryboard(name: "LaunchPlaceholderBoard", bundle: Bundle.main)
|
|
window.rootViewController = storyboard.instantiateViewController(withIdentifier: "LaunchPlaceholderController")
|
|
window.isHidden = false
|
|
|
|
switch state {
|
|
case .unlocked:
|
|
state = .locked(lockTime: lockTime)
|
|
case .locked:
|
|
// In case of Locked state don't want to update lockTime.
|
|
break
|
|
case .validatingPin:
|
|
// In case of ValidatingPin state we also don't want to do anything.
|
|
break
|
|
}
|
|
}
|
|
|
|
func unlock() {
|
|
KeychainManager().failedPinAttemptsNumber = nil
|
|
|
|
state = .unlocked
|
|
window.isHidden = true
|
|
}
|
|
|
|
func challengeUserToAuthorize(_ lockTime: CFTimeInterval) {
|
|
if window.rootViewController is EnterPinController {
|
|
// already showing pin controller
|
|
return
|
|
}
|
|
|
|
if shouldUseTouchID() {
|
|
state = .validatingPin
|
|
|
|
LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
|
|
localizedReason: String(localized: "pin_touch_id_description"),
|
|
reply: { [weak self] success, error in
|
|
DispatchQueue.main.async {
|
|
self?.state = .locked(lockTime: lockTime)
|
|
|
|
success ? self?.unlock() : self?.showValidatePinController()
|
|
}
|
|
})
|
|
}
|
|
else {
|
|
showValidatePinController()
|
|
}
|
|
}
|
|
|
|
func showValidatePinController() {
|
|
let settings = submanagerObjects.getProfileSettings()
|
|
guard let pin = settings.unlockPinCode else {
|
|
fatalError("pin shouldn't be nil")
|
|
}
|
|
|
|
let failedAttempts = KeychainManager().failedPinAttemptsNumber ?? 0
|
|
|
|
let controller = EnterPinController(theme: theme, state: .validatePin(validPin: pin))
|
|
controller.topText = String(localized: "pin_enter_to_unlock")
|
|
controller.descriptionText =
|
|
failedAttempts > 0 ?
|
|
String(localized: "pin_failed_attempts", "\(failedAttempts)") :
|
|
nil
|
|
controller.delegate = self
|
|
window.rootViewController = controller
|
|
}
|
|
|
|
func isPinDateExpired(_ lockTime: CFTimeInterval) -> Bool {
|
|
let settings = submanagerObjects.getProfileSettings()
|
|
let delta = CACurrentMediaTime() - lockTime
|
|
|
|
switch settings.lockTimeout {
|
|
case .Immediately:
|
|
return true
|
|
case .Seconds30:
|
|
return delta > 30
|
|
case .Minute1:
|
|
return delta > 60
|
|
case .Minute2:
|
|
return delta > (60 * 2)
|
|
case .Minute5:
|
|
return delta > (60 * 5)
|
|
}
|
|
}
|
|
|
|
func shouldUseTouchID() -> Bool {
|
|
guard submanagerObjects.getProfileSettings().useTouchID else {
|
|
return false
|
|
}
|
|
|
|
guard LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|