From 4eda5c76310f64c9cd411612d2d75d2ee68c6018 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Fri, 27 Oct 2017 13:50:53 -0700 Subject: [PATCH 01/23] Prototype of using privacy screen and LocalAuthentication - Adds Component that: - Checks if device can use LocalAuthentication - Checks if a successful auth challenge has occurred Given that LocalAuthentication is available on the device: When the application becomes foreground after launching a privacy screen is presented. A successful LocalAuthentication dismisses the privacy screen. When the application enters the background state the privacy screen is presented. This prevents tokens from being displayed during app switching. None of the keychain items are using LocalAuthentication for encryption. This is purely UI related so the security/encryption of the keychain items have not been changed by this feature. Tokens are still readable/displayable by the app no matter what the state of the LocalAuthentication challenge is. --- Authenticator/Source/AppController.swift | 17 +++++ Authenticator/Source/OTPAppDelegate.swift | 8 ++ Authenticator/Source/Root.swift | 76 ++++++++++++++++++- Authenticator/Source/RootViewController.swift | 46 +++++++++++ Authenticator/Source/RootViewModel.swift | 1 + 5 files changed, 147 insertions(+), 1 deletion(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 745c1c80..0df7c361 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -28,6 +28,7 @@ import UIKit import SafariServices import OneTimePassword import SVProgressHUD +import LocalAuthentication class AppController { private let store: TokenStore @@ -214,6 +215,14 @@ class AppController { func addTokenFromURL(_ token: Token) { handleAction(.addTokenFromURL(token)) } + + func checkForLocalAuth() { + handleAction(Auth.checkForLocalAuth()) + } + + func enablePrivacy() { + handleAction(.authAction(.enablePrivacy)) + } } private extension DisplayTime { @@ -225,3 +234,11 @@ private extension DisplayTime { return DisplayTime(date: Date()) } } + +private extension Auth { + static func checkForLocalAuth() -> Root.Action { + let context = LAContext() + let canUseLocalAuth = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) + return .authAction(.enableLocalAuth(isEnabled: canUseLocalAuth)) + } +} diff --git a/Authenticator/Source/OTPAppDelegate.swift b/Authenticator/Source/OTPAppDelegate.swift index 46155362..d25cc3ad 100644 --- a/Authenticator/Source/OTPAppDelegate.swift +++ b/Authenticator/Source/OTPAppDelegate.swift @@ -48,6 +48,14 @@ class OTPAppDelegate: UIResponder, UIApplicationDelegate { return true } + func applicationDidBecomeActive(_ application: UIApplication) { + app.checkForLocalAuth() + } + + func applicationDidEnterBackground(_ application: UIApplication) { + app.enablePrivacy() + } + func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { if let token = Token(url: url) { let message = "Do you want to add a token for “\(token.name)”?" diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index a9ed94b6..eafd14c5 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -30,6 +30,7 @@ struct Root: Component { fileprivate var tokenList: TokenList fileprivate var modal: Modal fileprivate let deviceCanScan: Bool + fileprivate var auth: Auth fileprivate enum Modal { case none @@ -57,10 +58,69 @@ struct Root: Component { init(deviceCanScan: Bool) { tokenList = TokenList() modal = .none + auth = Auth() self.deviceCanScan = deviceCanScan } } +struct Auth: Component { + typealias ViewModel = AuthViewModel + var authAvailable: Bool = false + var authRequired: Bool = false + + enum Action { + case enableLocalAuth(isEnabled: Bool) + case enablePrivacy + case authResult(reply: Bool, error: Error?) + } + + enum Effect { + case authRequired + case authObtained + } + + var viewModel: AuthViewModel { + get { + return AuthViewModel(enabled: authAvailable && authRequired) + } + } + + mutating func update(_ action: Action) throws -> Effect? { + switch action { + case .enableLocalAuth(let isEnabled): + return try handleEnableLocalAuth(isEnabled) + case .enablePrivacy: + authRequired = true + return authAvailable ? .authRequired : nil + case .authResult(let reply, let error): + if reply { + authRequired = false + return .authObtained + } + return nil + } + } + + private mutating func handleEnableLocalAuth(_ shouldEnable: Bool ) throws -> Effect? { + // no change, no effect + if( authAvailable == shouldEnable ) { + return nil + } + authAvailable = shouldEnable + + // enabling after not being enabled, show privacy screen + if ( authAvailable ) { + return try update(.enablePrivacy) + } + return nil + } + +} + +struct AuthViewModel { + var enabled: Bool +} + // MARK: View extension Root { @@ -69,7 +129,8 @@ extension Root { func viewModel(for persistentTokens: [PersistentToken], at displayTime: DisplayTime) -> ViewModel { return ViewModel( tokenList: tokenList.viewModel(for: persistentTokens, at: displayTime), - modal: modal.viewModel + modal: modal.viewModel, + privacy: auth.viewModel ) } } @@ -88,6 +149,7 @@ extension Root { case dismissInfo case addTokenFromURL(Token) + case authAction(Auth.Action) } enum Event { @@ -166,6 +228,9 @@ extension Root { return .addToken(token, success: Event.addTokenFromURLSucceeded, failure: Event.addTokenFailed) + + case .authAction(let action): + return try auth.update(action).flatMap { handleAuthEffect($0) } } } catch { throw ComponentError(underlyingError: error, action: action, component: self) @@ -341,6 +406,15 @@ extension Root { return .openURL(url) } } + + private mutating func handleAuthEffect(_ effect: Auth.Effect) -> Effect? { + switch effect { + case .authRequired: + return nil + case .authObtained: + return nil + } + } } private extension Root.Modal { diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 7fadf21f..572b42a1 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -24,6 +24,7 @@ // import UIKit +import LocalAuthentication class OpaqueNavigationController: UINavigationController { override func viewDidLoad() { @@ -52,6 +53,7 @@ class RootViewController: OpaqueNavigationController { fileprivate var tokenListViewController: TokenListViewController fileprivate var modalNavController: UINavigationController? + fileprivate var authController: UIViewController? fileprivate let dispatchAction: (Root.Action) -> Void @@ -162,7 +164,10 @@ extension RootViewController { case .info(let infoListViewModel, let infoViewModel): updateWithInfoViewModels(infoListViewModel, infoViewModel) + } + updateWithAuthViewModel(viewModel.privacy) + currentViewModel = viewModel } @@ -187,6 +192,47 @@ extension RootViewController { ) presentViewControllers([infoListViewController, infoViewController]) } + + private func updateWithAuthViewModel(_ viewModel: AuthViewModel) { + if viewModel.enabled == currentViewModel.privacy.enabled { + return + } + if viewModel.enabled { + if authController == nil { + authController = UIViewController() + authController?.view.backgroundColor = UIColor.purple + let button = UIButton(type: .roundedRect) + button.setTitle("Hello Wrold", for: .normal) + button.addTarget(self, action: #selector(authChallenge), for: .touchUpInside) + button.sizeToFit() + authController?.view.addSubview(button) + button.center = authController!.view.center + authController?.modalPresentationStyle = .overFullScreen + } + + guard let controller = authController else { + return + } + if let navController = modalNavController { + navController.topViewController?.present(controller, animated: false) + } else { + present(controller, animated: false) + } + + } + if !viewModel.enabled { + authController?.presentingViewController?.dismiss(animated: true) + } + } + + @objc private func authChallenge() { + let context = LAContext() + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (reply, error) in + DispatchQueue.main.async { + self.dispatchAction(.authAction(.authResult(reply: reply, error: error))) + } + } + } } private func compose(_ transform: @escaping (A) -> B, _ handler: @escaping (B) -> C) -> (A) -> C { diff --git a/Authenticator/Source/RootViewModel.swift b/Authenticator/Source/RootViewModel.swift index 0d5df25a..a1b7e780 100644 --- a/Authenticator/Source/RootViewModel.swift +++ b/Authenticator/Source/RootViewModel.swift @@ -26,6 +26,7 @@ struct RootViewModel { let tokenList: TokenList.ViewModel let modal: ModalViewModel + let privacy: Auth.ViewModel enum ModalViewModel { case none From 40ba192fd1c3373e87ef5ebec0bb371d62e71572 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Fri, 27 Oct 2017 15:18:17 -0700 Subject: [PATCH 02/23] A less abrasive prototype UI --- Authenticator/Source/RootViewController.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 572b42a1..ec6bee20 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -200,9 +200,10 @@ extension RootViewController { if viewModel.enabled { if authController == nil { authController = UIViewController() - authController?.view.backgroundColor = UIColor.purple + authController?.view.backgroundColor = UIColor.otpBackgroundColor let button = UIButton(type: .roundedRect) - button.setTitle("Hello Wrold", for: .normal) + button.setTitleColor(UIColor.otpForegroundColor, for: .normal) + button.setTitle("Unlock", for: .normal) button.addTarget(self, action: #selector(authChallenge), for: .touchUpInside) button.sizeToFit() authController?.view.addSubview(button) @@ -213,15 +214,16 @@ extension RootViewController { guard let controller = authController else { return } - if let navController = modalNavController { - navController.topViewController?.present(controller, animated: false) + if let presented = presentedViewController { + presented.present(controller, animated: false) + return } else { present(controller, animated: false) } - } if !viewModel.enabled { authController?.presentingViewController?.dismiss(animated: true) + authController = nil } } From 5c9d8a90ebf8f198c8bb258f856e5f63b7f89e98 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Thu, 27 Sep 2018 11:42:31 -0700 Subject: [PATCH 03/23] Conforms to changed protocol update(with: ActionType) method --- Authenticator/Source/Root.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index 1ef92141..b7b685fd 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -85,14 +85,14 @@ struct Auth: Component { } } - mutating func update(_ action: Action) throws -> Effect? { + mutating func update(with action: Action) throws -> Effect? { switch action { case .enableLocalAuth(let isEnabled): return try handleEnableLocalAuth(isEnabled) case .enablePrivacy: authRequired = true return authAvailable ? .authRequired : nil - case .authResult(let reply, let error): + case .authResult(let reply, _): if reply { authRequired = false return .authObtained @@ -110,7 +110,7 @@ struct Auth: Component { // enabling after not being enabled, show privacy screen if ( authAvailable ) { - return try update(.enablePrivacy) + return try update(with: .enablePrivacy) } return nil } @@ -250,7 +250,7 @@ extension Root { failure: Event.addTokenFailed) case .authAction(let action): - return try auth.update(action).flatMap { handleAuthEffect($0) } + return try auth.update(with: action).flatMap { handleAuthEffect($0) } } } catch { throw ComponentError(underlyingError: error, action: action, component: self) From 9e456c0f20985351959c8ec27a928194ada08c59 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Sun, 23 Dec 2018 21:27:13 -0500 Subject: [PATCH 04/23] Move the Auth component to a dedicated file --- Authenticator.xcodeproj/project.pbxproj | 4 ++ Authenticator/Source/Auth.swift | 82 +++++++++++++++++++++++++ Authenticator/Source/Root.swift | 58 ----------------- 3 files changed, 86 insertions(+), 58 deletions(-) create mode 100644 Authenticator/Source/Auth.swift diff --git a/Authenticator.xcodeproj/project.pbxproj b/Authenticator.xcodeproj/project.pbxproj index e5e11933..6da083d2 100644 --- a/Authenticator.xcodeproj/project.pbxproj +++ b/Authenticator.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ C944A5571A7ECB0800E08B1E /* OneTimePassword.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = C944A5561A7ECB0800E08B1E /* OneTimePassword.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C944A5591A7ECB3100E08B1E /* Base32.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = C944A5581A7ECB3100E08B1E /* Base32.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C968D1151CB4C639004ED7BB /* DisplayTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = C968D1141CB4C639004ED7BB /* DisplayTime.swift */; }; + C97350D421D07AE9004F5118 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97350D321D07AE9004F5118 /* Auth.swift */; }; C97CDF261BEEA66A00D64406 /* SVProgressHUD.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = C97CDF241BEEA49300D64406 /* SVProgressHUD.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C97CDF2C1BEEC90100D64406 /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97CDF2B1BEEC90100D64406 /* QRScanner.swift */; }; C983AB74197F98FC00975003 /* OTPAuthenticatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C983AB73197F98FC00975003 /* OTPAuthenticatorTests.m */; }; @@ -141,6 +142,7 @@ C959A63E190A69E60042DEC0 /* GenerateIcons.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = GenerateIcons.sh; sourceTree = ""; }; C968D1141CB4C639004ED7BB /* DisplayTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayTime.swift; sourceTree = ""; }; C96E60561DBC5F1B00484823 /* .codecov.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .codecov.yml; sourceTree = ""; }; + C97350D321D07AE9004F5118 /* Auth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; C9776E3318518801003D53CB /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; C97CDF041BEE927200D64406 /* AUTHORS */ = {isa = PBXFileReference; lastKnownFileType = text; path = AUTHORS; sourceTree = ""; }; C97CDF241BEEA49300D64406 /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SVProgressHUD.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -387,6 +389,7 @@ isa = PBXGroup; children = ( C93BD6221C167CD100FFFB8F /* Root.swift */, + C97350D321D07AE9004F5118 /* Auth.swift */, C93BD6281C168EBF00FFFB8F /* RootViewModel.swift */, C93BD6241C16841D00FFFB8F /* RootViewController.swift */, ); @@ -706,6 +709,7 @@ C9E3FB9A1E281CBC00EFA8BB /* TokenScanner.swift in Sources */, C93BD6291C168EBF00FFFB8F /* RootViewModel.swift in Sources */, C9F7A8611C4D90B50082E5AE /* TokenStore.swift in Sources */, + C97350D421D07AE9004F5118 /* Auth.swift in Sources */, C9CC09511BA903B7008C54FE /* TokenFormViewController.swift in Sources */, C98B1ECC1AB3CF0700C59E53 /* TokenRowCell.swift in Sources */, C910ADC11BF0315A00C988F5 /* TokenList.swift in Sources */, diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift new file mode 100644 index 00000000..513732da --- /dev/null +++ b/Authenticator/Source/Auth.swift @@ -0,0 +1,82 @@ +// +// Auth.swift +// Authenticator +// +// Copyright (c) 2017-2018 Authenticator authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +struct Auth: Component { + typealias ViewModel = AuthViewModel + var authAvailable: Bool = false + var authRequired: Bool = false + + enum Action { + case enableLocalAuth(isEnabled: Bool) + case enablePrivacy + case authResult(reply: Bool, error: Error?) + } + + enum Effect { + case authRequired + case authObtained + } + + var viewModel: AuthViewModel { + get { + return AuthViewModel(enabled: authAvailable && authRequired) + } + } + + mutating func update(with action: Action) throws -> Effect? { + switch action { + case .enableLocalAuth(let isEnabled): + return try handleEnableLocalAuth(isEnabled) + case .enablePrivacy: + authRequired = true + return authAvailable ? .authRequired : nil + case .authResult(let reply, _): + if reply { + authRequired = false + return .authObtained + } + return nil + } + } + + private mutating func handleEnableLocalAuth(_ shouldEnable: Bool ) throws -> Effect? { + // no change, no effect + if( authAvailable == shouldEnable ) { + return nil + } + authAvailable = shouldEnable + + // enabling after not being enabled, show privacy screen + if ( authAvailable ) { + return try update(with: .enablePrivacy) + } + return nil + } + +} + +struct AuthViewModel { + var enabled: Bool +} diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index 9720ddc7..83ee78ca 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -63,64 +63,6 @@ struct Root: Component { } } -struct Auth: Component { - typealias ViewModel = AuthViewModel - var authAvailable: Bool = false - var authRequired: Bool = false - - enum Action { - case enableLocalAuth(isEnabled: Bool) - case enablePrivacy - case authResult(reply: Bool, error: Error?) - } - - enum Effect { - case authRequired - case authObtained - } - - var viewModel: AuthViewModel { - get { - return AuthViewModel(enabled: authAvailable && authRequired) - } - } - - mutating func update(with action: Action) throws -> Effect? { - switch action { - case .enableLocalAuth(let isEnabled): - return try handleEnableLocalAuth(isEnabled) - case .enablePrivacy: - authRequired = true - return authAvailable ? .authRequired : nil - case .authResult(let reply, _): - if reply { - authRequired = false - return .authObtained - } - return nil - } - } - - private mutating func handleEnableLocalAuth(_ shouldEnable: Bool ) throws -> Effect? { - // no change, no effect - if( authAvailable == shouldEnable ) { - return nil - } - authAvailable = shouldEnable - - // enabling after not being enabled, show privacy screen - if ( authAvailable ) { - return try update(with: .enablePrivacy) - } - return nil - } - -} - -struct AuthViewModel { - var enabled: Bool -} - // MARK: View extension Root { From c1b1f9b736f41f3623ba75afaa82eba85a4933a0 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Sun, 23 Dec 2018 21:30:49 -0500 Subject: [PATCH 05/23] Clean up several lint issues --- Authenticator/Source/Auth.swift | 9 ++++----- Authenticator/Source/RootViewController.swift | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 513732da..31c8b85c 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -25,6 +25,7 @@ struct Auth: Component { typealias ViewModel = AuthViewModel + var authAvailable: Bool = false var authRequired: Bool = false @@ -40,9 +41,7 @@ struct Auth: Component { } var viewModel: AuthViewModel { - get { - return AuthViewModel(enabled: authAvailable && authRequired) - } + return AuthViewModel(enabled: authAvailable && authRequired) } mutating func update(with action: Action) throws -> Effect? { @@ -63,13 +62,13 @@ struct Auth: Component { private mutating func handleEnableLocalAuth(_ shouldEnable: Bool ) throws -> Effect? { // no change, no effect - if( authAvailable == shouldEnable ) { + if authAvailable == shouldEnable { return nil } authAvailable = shouldEnable // enabling after not being enabled, show privacy screen - if ( authAvailable ) { + if authAvailable { return try update(with: .enablePrivacy) } return nil diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 10f7f8a1..8bd03571 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -241,7 +241,8 @@ extension RootViewController { } } - @objc private func authChallenge() { + @objc + private func authChallenge() { let context = LAContext() context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (reply, error) in DispatchQueue.main.async { From a6322d836b45d5d1d70972e2e0b9ee19109c9592 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Sun, 23 Dec 2018 21:34:36 -0500 Subject: [PATCH 06/23] Nest the Auth view model type inside the component type --- Authenticator/Source/Auth.swift | 22 ++++++++++--------- Authenticator/Source/RootViewController.swift | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 31c8b85c..f73458b8 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -24,11 +24,21 @@ // struct Auth: Component { - typealias ViewModel = AuthViewModel - var authAvailable: Bool = false var authRequired: Bool = false + // MARK: View + + struct ViewModel { + var enabled: Bool + } + + var viewModel: ViewModel { + return ViewModel(enabled: authAvailable && authRequired) + } + + // MARK: Update + enum Action { case enableLocalAuth(isEnabled: Bool) case enablePrivacy @@ -40,10 +50,6 @@ struct Auth: Component { case authObtained } - var viewModel: AuthViewModel { - return AuthViewModel(enabled: authAvailable && authRequired) - } - mutating func update(with action: Action) throws -> Effect? { switch action { case .enableLocalAuth(let isEnabled): @@ -75,7 +81,3 @@ struct Auth: Component { } } - -struct AuthViewModel { - var enabled: Bool -} diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 8bd03571..163883e2 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -207,7 +207,7 @@ extension RootViewController { presentViewControllers([viewControllerA, viewControllerB]) } - private func updateWithAuthViewModel(_ viewModel: AuthViewModel) { + private func updateWithAuthViewModel(_ viewModel: Auth.ViewModel) { if viewModel.enabled == currentViewModel.privacy.enabled { return } From f1a056ebcb2e046f8e27f9b8407b305b0171bec3 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Sun, 23 Dec 2018 21:56:37 -0500 Subject: [PATCH 07/23] Refactor checkForLocalAuth() --- Authenticator/Source/AppController.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index f52ed9d9..b2d12122 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -212,7 +212,10 @@ class AppController { } func checkForLocalAuth() { - handleAction(Auth.checkForLocalAuth()) + let context = LAContext() + let canUseLocalAuth = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) + let action = Root.Action.authAction(.enableLocalAuth(isEnabled: canUseLocalAuth)) + handleAction(action) } func enablePrivacy() { @@ -267,11 +270,3 @@ private extension DisplayTime { return DisplayTime(date: Date()) } } - -private extension Auth { - static func checkForLocalAuth() -> Root.Action { - let context = LAContext() - let canUseLocalAuth = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) - return .authAction(.enableLocalAuth(isEnabled: canUseLocalAuth)) - } -} From c04dd399c6ac9633f8b8eaf0394b1faa9eeadba1 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 14:11:19 -0500 Subject: [PATCH 08/23] Add an NSFaceIDUsageDescription --- Authenticator/Resources/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Authenticator/Resources/Info.plist b/Authenticator/Resources/Info.plist index 7ca5c289..6a620d41 100644 --- a/Authenticator/Resources/Info.plist +++ b/Authenticator/Resources/Info.plist @@ -39,6 +39,8 @@ NSCameraUsageDescription Authenticator can import tokens by scanning QR codes. + NSFaceIDUsageDescription + Authenticator uses Face ID to protect your tokens. UILaunchStoryboardName LaunchScreen UIStatusBarStyle From 94015a8601e95427940027499ae88aadd96baf9a Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 14:40:34 -0500 Subject: [PATCH 09/23] Delegate LocalAuthentication challenge to the AppController --- Authenticator/Source/AppController.swift | 12 ++++++++++++ Authenticator/Source/Auth.swift | 4 ++++ Authenticator/Source/Root.swift | 4 ++++ Authenticator/Source/RootViewController.swift | 12 +++--------- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index b2d12122..93ebdcd7 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -156,6 +156,9 @@ class AppController { case let .deletePersistentToken(persistentToken, failure): confirmDeletion(of: persistentToken, failure: failure) + case .authenticateUser: + authenticateUser() + case let .showErrorMessage(message): SVProgressHUD.showError(withStatus: message) generateHapticFeedback(for: .error) @@ -222,6 +225,15 @@ class AppController { handleAction(.authAction(.enablePrivacy)) } + private func authenticateUser() { + let context = LAContext() + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (reply, error) in + DispatchQueue.main.async { [weak self] in + self?.handleAction(.authAction(.authResult(reply: reply, error: error))) + } + } + } + private func confirmDeletion(of persistentToken: PersistentToken, failure: @escaping (Error) -> Root.Event) { let messagePrefix = persistentToken.token.displayName.map({ "The token “\($0)”" }) ?? "The unnamed token" let message = messagePrefix + " will be permanently deleted from this device." diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index f73458b8..21fbd3a2 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -42,10 +42,12 @@ struct Auth: Component { enum Action { case enableLocalAuth(isEnabled: Bool) case enablePrivacy + case tryToUnlock case authResult(reply: Bool, error: Error?) } enum Effect { + case authenticateUser case authRequired case authObtained } @@ -57,6 +59,8 @@ struct Auth: Component { case .enablePrivacy: authRequired = true return authAvailable ? .authRequired : nil + case .tryToUnlock: + return .authenticateUser case .authResult(let reply, _): if reply { authRequired = false diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index 83ee78ca..c85a6246 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -126,6 +126,8 @@ extension Root { case deletePersistentToken(PersistentToken, failure: (Error) -> Event) + case authenticateUser + case showErrorMessage(String) case showSuccessMessage(String) case showApplicationSettings @@ -332,6 +334,8 @@ extension Root { private mutating func handleAuthEffect(_ effect: Auth.Effect) -> Effect? { switch effect { + case .authenticateUser: + return .authenticateUser case .authRequired: return nil case .authObtained: diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 163883e2..c2ccd4a2 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -24,7 +24,6 @@ // import UIKit -import LocalAuthentication class OpaqueNavigationController: UINavigationController { override func viewDidLoad() { @@ -218,7 +217,7 @@ extension RootViewController { let button = UIButton(type: .roundedRect) button.setTitleColor(UIColor.otpForegroundColor, for: .normal) button.setTitle("Unlock", for: .normal) - button.addTarget(self, action: #selector(authChallenge), for: .touchUpInside) + button.addTarget(self, action: #selector(tryToUnlock), for: .touchUpInside) button.sizeToFit() authController?.view.addSubview(button) button.center = authController!.view.center @@ -242,13 +241,8 @@ extension RootViewController { } @objc - private func authChallenge() { - let context = LAContext() - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (reply, error) in - DispatchQueue.main.async { - self.dispatchAction(.authAction(.authResult(reply: reply, error: error))) - } - } + private func tryToUnlock() { + dispatchAction(.authAction(.tryToUnlock)) } } From eade8b7da8d4983f9a6cc3b59cfc729d2397865e Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 15:13:37 -0500 Subject: [PATCH 10/23] Re-route authentication events --- Authenticator/Source/AppController.swift | 17 ++++++++++----- Authenticator/Source/Auth.swift | 27 +++++++++++++++++------- Authenticator/Source/Root.swift | 15 ++++++++++--- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 93ebdcd7..07d2da1e 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -156,8 +156,8 @@ class AppController { case let .deletePersistentToken(persistentToken, failure): confirmDeletion(of: persistentToken, failure: failure) - case .authenticateUser: - authenticateUser() + case let .authenticateUser(success, failure): + authenticateUser(success: success, failure: failure) case let .showErrorMessage(message): SVProgressHUD.showError(withStatus: message) @@ -225,11 +225,18 @@ class AppController { handleAction(.authAction(.enablePrivacy)) } - private func authenticateUser() { + private func authenticateUser(success successEvent: Root.Event, failure: @escaping (Error) -> Root.Event) { let context = LAContext() - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (reply, error) in + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (success, error) in DispatchQueue.main.async { [weak self] in - self?.handleAction(.authAction(.authResult(reply: reply, error: error))) + if let error = error { + assert(success == false) + let failureEvent = failure(error) + self?.handleEvent(failureEvent) + } else { + assert(success == true) + self?.handleEvent(successEvent) + } } } } diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 21fbd3a2..8cb3b4d0 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -43,15 +43,19 @@ struct Auth: Component { case enableLocalAuth(isEnabled: Bool) case enablePrivacy case tryToUnlock - case authResult(reply: Bool, error: Error?) } enum Effect { - case authenticateUser + case authenticateUser(success: Event, failure: (Error) -> Event) case authRequired case authObtained } + enum Event { + case authenticationSucceeded + case authenticationFailed(Error) + } + mutating func update(with action: Action) throws -> Effect? { switch action { case .enableLocalAuth(let isEnabled): @@ -60,12 +64,19 @@ struct Auth: Component { authRequired = true return authAvailable ? .authRequired : nil case .tryToUnlock: - return .authenticateUser - case .authResult(let reply, _): - if reply { - authRequired = false - return .authObtained - } + return .authenticateUser(success: .authenticationSucceeded, + failure: Event.authenticationFailed) + } + } + + mutating func update(with event: Event) -> Effect? { + switch event { + case .authenticationSucceeded: + authRequired = false + return .authObtained + + case .authenticationFailed(let error): + print(error) // TODO: Improve error handling return nil } } diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index c85a6246..43e54ef6 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -106,6 +106,8 @@ extension Root { case updateTokenFailed(Error) case moveTokenFailed(Error) case deleteTokenFailed(Error) + + case authenticationEvent(Auth.Event) } enum Effect { @@ -126,7 +128,7 @@ extension Root { case deletePersistentToken(PersistentToken, failure: (Error) -> Event) - case authenticateUser + case authenticateUser(success: Event, failure: (Error) -> Event) case showErrorMessage(String) case showSuccessMessage(String) @@ -201,6 +203,12 @@ extension Root { return .showErrorMessage("Failed to move token.") case .deleteTokenFailed: return .showErrorMessage("Failed to delete token.") + + case .authenticationEvent(let authEvent): + let effect = auth.update(with: authEvent) + return effect.flatMap { effect in + handleAuthEffect(effect) + } } } @@ -334,8 +342,9 @@ extension Root { private mutating func handleAuthEffect(_ effect: Auth.Effect) -> Effect? { switch effect { - case .authenticateUser: - return .authenticateUser + case let .authenticateUser(success, failure): + return .authenticateUser(success: .authenticationEvent(success), + failure: { .authenticationEvent(failure($0)) }) case .authRequired: return nil case .authObtained: From 533186488a78850131a95bdc962a4cb6d19530d5 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 15:18:51 -0500 Subject: [PATCH 11/23] Delete unused Auth Effects --- Authenticator/Source/Auth.swift | 8 ++++---- Authenticator/Source/Root.swift | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 8cb3b4d0..bc7bb9b3 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -47,8 +47,6 @@ struct Auth: Component { enum Effect { case authenticateUser(success: Event, failure: (Error) -> Event) - case authRequired - case authObtained } enum Event { @@ -60,9 +58,11 @@ struct Auth: Component { switch action { case .enableLocalAuth(let isEnabled): return try handleEnableLocalAuth(isEnabled) + case .enablePrivacy: authRequired = true - return authAvailable ? .authRequired : nil + return nil + case .tryToUnlock: return .authenticateUser(success: .authenticationSucceeded, failure: Event.authenticationFailed) @@ -73,7 +73,7 @@ struct Auth: Component { switch event { case .authenticationSucceeded: authRequired = false - return .authObtained + return nil case .authenticationFailed(let error): print(error) // TODO: Improve error handling diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index 43e54ef6..f20ac085 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -345,10 +345,6 @@ extension Root { case let .authenticateUser(success, failure): return .authenticateUser(success: .authenticationEvent(success), failure: { .authenticationEvent(failure($0)) }) - case .authRequired: - return nil - case .authObtained: - return nil } } } From e9a9ad70f551a267ab0f48e904e3ea4bb802bdad Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 18:05:15 -0500 Subject: [PATCH 12/23] Refactor Auth actions --- Authenticator/Source/Auth.swift | 35 +++++++++++++-------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index bc7bb9b3..53536e2d 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -25,7 +25,7 @@ struct Auth: Component { var authAvailable: Bool = false - var authRequired: Bool = false + var isLocked: Bool = false // MARK: View @@ -34,7 +34,7 @@ struct Auth: Component { } var viewModel: ViewModel { - return ViewModel(enabled: authAvailable && authRequired) + return ViewModel(enabled: authAvailable && isLocked) } // MARK: Update @@ -56,11 +56,19 @@ struct Auth: Component { mutating func update(with action: Action) throws -> Effect? { switch action { - case .enableLocalAuth(let isEnabled): - return try handleEnableLocalAuth(isEnabled) + case .enableLocalAuth(let shouldEnable): + if authAvailable != shouldEnable { + authAvailable = shouldEnable + + // enabling after not being enabled, show privacy screen + if authAvailable { + isLocked = true + } + } + return nil case .enablePrivacy: - authRequired = true + isLocked = true return nil case .tryToUnlock: @@ -72,7 +80,7 @@ struct Auth: Component { mutating func update(with event: Event) -> Effect? { switch event { case .authenticationSucceeded: - authRequired = false + isLocked = false return nil case .authenticationFailed(let error): @@ -80,19 +88,4 @@ struct Auth: Component { return nil } } - - private mutating func handleEnableLocalAuth(_ shouldEnable: Bool ) throws -> Effect? { - // no change, no effect - if authAvailable == shouldEnable { - return nil - } - authAvailable = shouldEnable - - // enabling after not being enabled, show privacy screen - if authAvailable { - return try update(with: .enablePrivacy) - } - return nil - } - } From 5f2235f19c79cb13a0a4ddebb1aaa5311f1ad950 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 18:28:55 -0500 Subject: [PATCH 13/23] Lock the screen on Event.applicationDidEnterBackground --- Authenticator/Source/AppController.swift | 4 ++-- Authenticator/Source/Auth.swift | 10 +++++----- Authenticator/Source/OTPAppDelegate.swift | 2 +- Authenticator/Source/Root.swift | 7 +++++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 07d2da1e..9c8ac773 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -221,8 +221,8 @@ class AppController { handleAction(action) } - func enablePrivacy() { - handleAction(.authAction(.enablePrivacy)) + func applicationDidEnterBackground() { + handleEvent(.applicationDidEnterBackground) } private func authenticateUser(success successEvent: Root.Event, failure: @escaping (Error) -> Root.Event) { diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 53536e2d..138b0410 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -41,7 +41,6 @@ struct Auth: Component { enum Action { case enableLocalAuth(isEnabled: Bool) - case enablePrivacy case tryToUnlock } @@ -50,6 +49,7 @@ struct Auth: Component { } enum Event { + case applicationDidEnterBackground case authenticationSucceeded case authenticationFailed(Error) } @@ -67,10 +67,6 @@ struct Auth: Component { } return nil - case .enablePrivacy: - isLocked = true - return nil - case .tryToUnlock: return .authenticateUser(success: .authenticationSucceeded, failure: Event.authenticationFailed) @@ -79,6 +75,10 @@ struct Auth: Component { mutating func update(with event: Event) -> Effect? { switch event { + case .applicationDidEnterBackground: + isLocked = true + return nil + case .authenticationSucceeded: isLocked = false return nil diff --git a/Authenticator/Source/OTPAppDelegate.swift b/Authenticator/Source/OTPAppDelegate.swift index 3a46c30c..df2f3525 100644 --- a/Authenticator/Source/OTPAppDelegate.swift +++ b/Authenticator/Source/OTPAppDelegate.swift @@ -61,7 +61,7 @@ class OTPAppDelegate: UIResponder, UIApplicationDelegate { } func applicationDidEnterBackground(_ application: UIApplication) { - app.enablePrivacy() + app.applicationDidEnterBackground() } func applicationWillEnterForeground(_ application: UIApplication) { diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index f20ac085..a35fd96f 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -107,6 +107,7 @@ extension Root { case moveTokenFailed(Error) case deleteTokenFailed(Error) + case applicationDidEnterBackground case authenticationEvent(Auth.Event) } @@ -204,6 +205,12 @@ extension Root { case .deleteTokenFailed: return .showErrorMessage("Failed to delete token.") + case .applicationDidEnterBackground: + let effect = auth.update(with: .applicationDidEnterBackground) + return effect.flatMap { effect in + handleAuthEffect(effect) + } + case .authenticationEvent(let authEvent): let effect = auth.update(with: authEvent) return effect.flatMap { effect in From d4fee61f71d977c41ecf5d1f4573c22e0ee40cd6 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 18:39:11 -0500 Subject: [PATCH 14/23] Lock the screen earlier, in applicationWillResignActive --- Authenticator/Source/AppController.swift | 4 ++-- Authenticator/Source/Auth.swift | 4 ++-- Authenticator/Source/OTPAppDelegate.swift | 4 ++-- Authenticator/Source/Root.swift | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 9c8ac773..42ac8c80 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -221,8 +221,8 @@ class AppController { handleAction(action) } - func applicationDidEnterBackground() { - handleEvent(.applicationDidEnterBackground) + func applicationWillResignActive() { + handleEvent(.applicationWillResignActive) } private func authenticateUser(success successEvent: Root.Event, failure: @escaping (Error) -> Root.Event) { diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 138b0410..7faca9fa 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -49,7 +49,7 @@ struct Auth: Component { } enum Event { - case applicationDidEnterBackground + case applicationWillResignActive case authenticationSucceeded case authenticationFailed(Error) } @@ -75,7 +75,7 @@ struct Auth: Component { mutating func update(with event: Event) -> Effect? { switch event { - case .applicationDidEnterBackground: + case .applicationWillResignActive: isLocked = true return nil diff --git a/Authenticator/Source/OTPAppDelegate.swift b/Authenticator/Source/OTPAppDelegate.swift index df2f3525..ba5df0ff 100644 --- a/Authenticator/Source/OTPAppDelegate.swift +++ b/Authenticator/Source/OTPAppDelegate.swift @@ -60,8 +60,8 @@ class OTPAppDelegate: UIResponder, UIApplicationDelegate { app.checkForLocalAuth() } - func applicationDidEnterBackground(_ application: UIApplication) { - app.applicationDidEnterBackground() + func applicationWillResignActive(_ application: UIApplication) { + app.applicationWillResignActive() } func applicationWillEnterForeground(_ application: UIApplication) { diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index a35fd96f..b108dba9 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -107,7 +107,7 @@ extension Root { case moveTokenFailed(Error) case deleteTokenFailed(Error) - case applicationDidEnterBackground + case applicationWillResignActive case authenticationEvent(Auth.Event) } @@ -205,8 +205,8 @@ extension Root { case .deleteTokenFailed: return .showErrorMessage("Failed to delete token.") - case .applicationDidEnterBackground: - let effect = auth.update(with: .applicationDidEnterBackground) + case .applicationWillResignActive: + let effect = auth.update(with: .applicationWillResignActive) return effect.flatMap { effect in handleAuthEffect(effect) } From f1b291bf4e20bf5c7fed72674d64cbeb054304f7 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 19:07:54 -0500 Subject: [PATCH 15/23] Automatically prompt for authentication on applicationDidBecomeActive --- Authenticator/Source/AppController.swift | 4 ++++ Authenticator/Source/Auth.swift | 10 ++++++++++ Authenticator/Source/OTPAppDelegate.swift | 1 + Authenticator/Source/Root.swift | 7 +++++++ 4 files changed, 22 insertions(+) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 42ac8c80..1a18b2bc 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -221,6 +221,10 @@ class AppController { handleAction(action) } + func applicationDidBecomeActive() { + handleEvent(.applicationDidBecomeActive) + } + func applicationWillResignActive() { handleEvent(.applicationWillResignActive) } diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 7faca9fa..b0b9878e 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -49,7 +49,9 @@ struct Auth: Component { } enum Event { + case applicationDidBecomeActive case applicationWillResignActive + case authenticationSucceeded case authenticationFailed(Error) } @@ -75,6 +77,14 @@ struct Auth: Component { mutating func update(with event: Event) -> Effect? { switch event { + case .applicationDidBecomeActive: + if isLocked { + return .authenticateUser(success: .authenticationSucceeded, + failure: Event.authenticationFailed) + } else { + return nil + } + case .applicationWillResignActive: isLocked = true return nil diff --git a/Authenticator/Source/OTPAppDelegate.swift b/Authenticator/Source/OTPAppDelegate.swift index ba5df0ff..4ca58aa8 100644 --- a/Authenticator/Source/OTPAppDelegate.swift +++ b/Authenticator/Source/OTPAppDelegate.swift @@ -58,6 +58,7 @@ class OTPAppDelegate: UIResponder, UIApplicationDelegate { func applicationDidBecomeActive(_ application: UIApplication) { app.checkForLocalAuth() + app.applicationDidBecomeActive() } func applicationWillResignActive(_ application: UIApplication) { diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index b108dba9..2432929c 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -107,6 +107,7 @@ extension Root { case moveTokenFailed(Error) case deleteTokenFailed(Error) + case applicationDidBecomeActive case applicationWillResignActive case authenticationEvent(Auth.Event) } @@ -205,6 +206,12 @@ extension Root { case .deleteTokenFailed: return .showErrorMessage("Failed to delete token.") + case .applicationDidBecomeActive: + let effect = auth.update(with: .applicationDidBecomeActive) + return effect.flatMap { effect in + handleAuthEffect(effect) + } + case .applicationWillResignActive: let effect = auth.update(with: .applicationWillResignActive) return effect.flatMap { effect in From 1aff3cc258a189f2626a9c272c101065f74fe3b9 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 22:41:20 -0500 Subject: [PATCH 16/23] Add an internal State enum to Auth --- Authenticator/Source/Auth.swift | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index b0b9878e..efe1573a 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -25,7 +25,21 @@ struct Auth: Component { var authAvailable: Bool = false - var isLocked: Bool = false + private var state: State = .unlocked + + private enum State: Equatable { + case unlocked + case locked + + var isLocked: Bool { + switch self { + case .unlocked: + return false + case .locked: + return true + } + } + } // MARK: View @@ -34,7 +48,7 @@ struct Auth: Component { } var viewModel: ViewModel { - return ViewModel(enabled: authAvailable && isLocked) + return ViewModel(enabled: authAvailable && state.isLocked) } // MARK: Update @@ -64,7 +78,7 @@ struct Auth: Component { // enabling after not being enabled, show privacy screen if authAvailable { - isLocked = true + state = .locked } } return nil @@ -78,7 +92,7 @@ struct Auth: Component { mutating func update(with event: Event) -> Effect? { switch event { case .applicationDidBecomeActive: - if isLocked { + if state.isLocked { return .authenticateUser(success: .authenticationSucceeded, failure: Event.authenticationFailed) } else { @@ -86,11 +100,11 @@ struct Auth: Component { } case .applicationWillResignActive: - isLocked = true + state = .locked return nil case .authenticationSucceeded: - isLocked = false + state = .unlocked return nil case .authenticationFailed(let error): From ca5319bedc9a0301e2a2409c08bca0bff036dd6f Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Mon, 24 Dec 2018 23:01:01 -0500 Subject: [PATCH 17/23] Don't automatically re-attempt failed or cancelled authentication --- Authenticator/Source/Auth.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index efe1573a..26883b20 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -29,7 +29,7 @@ struct Auth: Component { private enum State: Equatable { case unlocked - case locked + case locked(shouldAuthenticateAutomatically: Bool) var isLocked: Bool { switch self { @@ -78,7 +78,7 @@ struct Auth: Component { // enabling after not being enabled, show privacy screen if authAvailable { - state = .locked + state = .locked(shouldAuthenticateAutomatically: true) } } return nil @@ -92,7 +92,7 @@ struct Auth: Component { mutating func update(with event: Event) -> Effect? { switch event { case .applicationDidBecomeActive: - if state.isLocked { + if case .locked(shouldAuthenticateAutomatically: true) = state { return .authenticateUser(success: .authenticationSucceeded, failure: Event.authenticationFailed) } else { @@ -100,7 +100,9 @@ struct Auth: Component { } case .applicationWillResignActive: - state = .locked + // When the app becomes inactive, regardless of the current state, the app should lock and prepare to + // automatically authenticate when the app becomes active. + state = .locked(shouldAuthenticateAutomatically: true) return nil case .authenticationSucceeded: @@ -108,6 +110,11 @@ struct Auth: Component { return nil case .authenticationFailed(let error): + // If automatic authentication fails, the app should remain locked, but should not automatically try to + // authenticate again. + if case .locked(shouldAuthenticateAutomatically: true) = state { + state = .locked(shouldAuthenticateAutomatically: false) + } print(error) // TODO: Improve error handling return nil } From 06def31ca6a43335ec22ff95aac0a7d93fc80794 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Sat, 9 Mar 2019 14:44:53 -0500 Subject: [PATCH 18/23] Add a localizedReason for the password entry screen Face ID uses the NSFaceIDUsageDescription from Info.plist, but if Face ID fails and falls back to manual password entry, this string is used. --- Authenticator/Source/AppController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 1a18b2bc..3321699d 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -231,7 +231,8 @@ class AppController { private func authenticateUser(success successEvent: Root.Event, failure: @escaping (Error) -> Root.Event) { let context = LAContext() - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "LOLZ") { (success, error) in + let localizedReason = "The Authenticator screen is locked to protect your tokens." + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: localizedReason) { (success, error) in DispatchQueue.main.async { [weak self] in if let error = error { assert(success == false) From 11f51dc7103b7e8b3df141f8c42cd546533dcbad Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Thu, 14 Mar 2019 17:48:58 -0400 Subject: [PATCH 19/23] Lock the app on launch if local auth is available --- Authenticator/Source/AppController.swift | 11 ++++++++--- Authenticator/Source/Auth.swift | 16 ++++++++++++++-- Authenticator/Source/Root.swift | 4 ++-- Authenticator/Source/RootViewController.swift | 10 ++++------ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index 3321699d..f2e502bd 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -75,7 +75,8 @@ class AppController { // If this is a demo, show the scanner even in the simulator. let deviceCanScan = QRScanner.deviceCanScan || CommandLine.isDemo - component = Root(deviceCanScan: deviceCanScan) + let isScreenLockEnabled = AppController.canUseLocalAuth() + component = Root(deviceCanScan: deviceCanScan, screenLockEnabled: isScreenLockEnabled) } @objc @@ -214,9 +215,13 @@ class AppController { handleAction(.addTokenFromURL(token)) } - func checkForLocalAuth() { + static func canUseLocalAuth() -> Bool { let context = LAContext() - let canUseLocalAuth = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) + return context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) + } + + func checkForLocalAuth() { + let canUseLocalAuth = AppController.canUseLocalAuth() let action = Root.Action.authAction(.enableLocalAuth(isEnabled: canUseLocalAuth)) handleAction(action) } diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 26883b20..4a3f9325 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -24,8 +24,8 @@ // struct Auth: Component { - var authAvailable: Bool = false - private var state: State = .unlocked + var authAvailable: Bool + private var state: State private enum State: Equatable { case unlocked @@ -41,6 +41,17 @@ struct Auth: Component { } } + init(screenLockEnabled: Bool) { + if screenLockEnabled { + state = .locked(shouldAuthenticateAutomatically: true) + } else { + state = .unlocked + } + // Initially, we will assume that if the screen lock is enabled, LocalAuthentication is available. + // TODO: Don't conflate the screen lock feature being enabled with the availability of LocalAuthentication + authAvailable = screenLockEnabled + } + // MARK: View struct ViewModel { @@ -48,6 +59,7 @@ struct Auth: Component { } var viewModel: ViewModel { + // TODO: Incorporate auth availability into lock state, so it does not possibly override lock state here.r return ViewModel(enabled: authAvailable && state.isLocked) } diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index 2432929c..a91210b4 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -55,10 +55,10 @@ struct Root: Component { } } - init(deviceCanScan: Bool) { + init(deviceCanScan: Bool, screenLockEnabled: Bool) { tokenList = TokenList() modal = .none - auth = Auth() + auth = Auth(screenLockEnabled: screenLockEnabled) self.deviceCanScan = deviceCanScan } } diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index c2ccd4a2..2b32bbbc 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -66,6 +66,8 @@ class RootViewController: OpaqueNavigationController { super.init(nibName: nil, bundle: nil) self.viewControllers = [tokenListViewController] + + updateWithAuthViewModel(viewModel.privacy) } @available(*, unavailable) @@ -207,10 +209,7 @@ extension RootViewController { } private func updateWithAuthViewModel(_ viewModel: Auth.ViewModel) { - if viewModel.enabled == currentViewModel.privacy.enabled { - return - } - if viewModel.enabled { + if viewModel.enabled && authController?.presentingViewController == nil { if authController == nil { authController = UIViewController() authController?.view.backgroundColor = UIColor.otpBackgroundColor @@ -233,8 +232,7 @@ extension RootViewController { } else { present(controller, animated: false) } - } - if !viewModel.enabled { + } else if !viewModel.enabled && authController?.presentingViewController != nil { authController?.presentingViewController?.dismiss(animated: true) authController = nil } From cc905b8eab1a764d403ec3cbcf7ba56c66f1ba65 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Thu, 14 Mar 2019 17:58:54 -0400 Subject: [PATCH 20/23] Remove auth availability from screen lock state --- Authenticator/Source/AppController.swift | 6 ------ Authenticator/Source/Auth.swift | 19 +------------------ Authenticator/Source/OTPAppDelegate.swift | 1 - 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/Authenticator/Source/AppController.swift b/Authenticator/Source/AppController.swift index f2e502bd..c756ad8b 100644 --- a/Authenticator/Source/AppController.swift +++ b/Authenticator/Source/AppController.swift @@ -220,12 +220,6 @@ class AppController { return context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) } - func checkForLocalAuth() { - let canUseLocalAuth = AppController.canUseLocalAuth() - let action = Root.Action.authAction(.enableLocalAuth(isEnabled: canUseLocalAuth)) - handleAction(action) - } - func applicationDidBecomeActive() { handleEvent(.applicationDidBecomeActive) } diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/Auth.swift index 4a3f9325..2a86a9b6 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/Auth.swift @@ -24,7 +24,6 @@ // struct Auth: Component { - var authAvailable: Bool private var state: State private enum State: Equatable { @@ -47,9 +46,6 @@ struct Auth: Component { } else { state = .unlocked } - // Initially, we will assume that if the screen lock is enabled, LocalAuthentication is available. - // TODO: Don't conflate the screen lock feature being enabled with the availability of LocalAuthentication - authAvailable = screenLockEnabled } // MARK: View @@ -59,14 +55,12 @@ struct Auth: Component { } var viewModel: ViewModel { - // TODO: Incorporate auth availability into lock state, so it does not possibly override lock state here.r - return ViewModel(enabled: authAvailable && state.isLocked) + return ViewModel(enabled: state.isLocked) } // MARK: Update enum Action { - case enableLocalAuth(isEnabled: Bool) case tryToUnlock } @@ -84,17 +78,6 @@ struct Auth: Component { mutating func update(with action: Action) throws -> Effect? { switch action { - case .enableLocalAuth(let shouldEnable): - if authAvailable != shouldEnable { - authAvailable = shouldEnable - - // enabling after not being enabled, show privacy screen - if authAvailable { - state = .locked(shouldAuthenticateAutomatically: true) - } - } - return nil - case .tryToUnlock: return .authenticateUser(success: .authenticationSucceeded, failure: Event.authenticationFailed) diff --git a/Authenticator/Source/OTPAppDelegate.swift b/Authenticator/Source/OTPAppDelegate.swift index 4ca58aa8..f30ee0c1 100644 --- a/Authenticator/Source/OTPAppDelegate.swift +++ b/Authenticator/Source/OTPAppDelegate.swift @@ -57,7 +57,6 @@ class OTPAppDelegate: UIResponder, UIApplicationDelegate { } func applicationDidBecomeActive(_ application: UIApplication) { - app.checkForLocalAuth() app.applicationDidBecomeActive() } From bbd82fd52e5614db33875a957c094790b9056df8 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Thu, 14 Mar 2019 17:59:07 -0400 Subject: [PATCH 21/23] Add a FIXME for the initial screen lock UI --- Authenticator/Source/RootViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 2b32bbbc..9b9596b5 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -226,6 +226,7 @@ extension RootViewController { guard let controller = authController else { return } + // FIXME: This fails to present a model over a VC that has not yet been added to the window. if let presented = presentedViewController { presented.present(controller, animated: false) return From f2a005547c1d0e25c43c65010c1ecef3df4c0618 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Thu, 14 Mar 2019 18:16:47 -0400 Subject: [PATCH 22/23] Rename the ScreenLock component --- Authenticator.xcodeproj/project.pbxproj | 8 ++--- Authenticator/Source/Root.swift | 34 +++++++++---------- Authenticator/Source/RootViewController.swift | 8 ++--- Authenticator/Source/RootViewModel.swift | 2 +- .../Source/{Auth.swift => ScreenLock.swift} | 4 +-- 5 files changed, 28 insertions(+), 28 deletions(-) rename Authenticator/Source/{Auth.swift => ScreenLock.swift} (98%) diff --git a/Authenticator.xcodeproj/project.pbxproj b/Authenticator.xcodeproj/project.pbxproj index 6da083d2..7c506d13 100644 --- a/Authenticator.xcodeproj/project.pbxproj +++ b/Authenticator.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ C944A5571A7ECB0800E08B1E /* OneTimePassword.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = C944A5561A7ECB0800E08B1E /* OneTimePassword.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C944A5591A7ECB3100E08B1E /* Base32.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = C944A5581A7ECB3100E08B1E /* Base32.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C968D1151CB4C639004ED7BB /* DisplayTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = C968D1141CB4C639004ED7BB /* DisplayTime.swift */; }; - C97350D421D07AE9004F5118 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97350D321D07AE9004F5118 /* Auth.swift */; }; + C97350D421D07AE9004F5118 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97350D321D07AE9004F5118 /* ScreenLock.swift */; }; C97CDF261BEEA66A00D64406 /* SVProgressHUD.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = C97CDF241BEEA49300D64406 /* SVProgressHUD.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C97CDF2C1BEEC90100D64406 /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97CDF2B1BEEC90100D64406 /* QRScanner.swift */; }; C983AB74197F98FC00975003 /* OTPAuthenticatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C983AB73197F98FC00975003 /* OTPAuthenticatorTests.m */; }; @@ -142,7 +142,7 @@ C959A63E190A69E60042DEC0 /* GenerateIcons.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = GenerateIcons.sh; sourceTree = ""; }; C968D1141CB4C639004ED7BB /* DisplayTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayTime.swift; sourceTree = ""; }; C96E60561DBC5F1B00484823 /* .codecov.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .codecov.yml; sourceTree = ""; }; - C97350D321D07AE9004F5118 /* Auth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; + C97350D321D07AE9004F5118 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; C9776E3318518801003D53CB /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; C97CDF041BEE927200D64406 /* AUTHORS */ = {isa = PBXFileReference; lastKnownFileType = text; path = AUTHORS; sourceTree = ""; }; C97CDF241BEEA49300D64406 /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SVProgressHUD.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -389,7 +389,7 @@ isa = PBXGroup; children = ( C93BD6221C167CD100FFFB8F /* Root.swift */, - C97350D321D07AE9004F5118 /* Auth.swift */, + C97350D321D07AE9004F5118 /* ScreenLock.swift */, C93BD6281C168EBF00FFFB8F /* RootViewModel.swift */, C93BD6241C16841D00FFFB8F /* RootViewController.swift */, ); @@ -709,7 +709,7 @@ C9E3FB9A1E281CBC00EFA8BB /* TokenScanner.swift in Sources */, C93BD6291C168EBF00FFFB8F /* RootViewModel.swift in Sources */, C9F7A8611C4D90B50082E5AE /* TokenStore.swift in Sources */, - C97350D421D07AE9004F5118 /* Auth.swift in Sources */, + C97350D421D07AE9004F5118 /* ScreenLock.swift in Sources */, C9CC09511BA903B7008C54FE /* TokenFormViewController.swift in Sources */, C98B1ECC1AB3CF0700C59E53 /* TokenRowCell.swift in Sources */, C910ADC11BF0315A00C988F5 /* TokenList.swift in Sources */, diff --git a/Authenticator/Source/Root.swift b/Authenticator/Source/Root.swift index a91210b4..321ee1cf 100644 --- a/Authenticator/Source/Root.swift +++ b/Authenticator/Source/Root.swift @@ -30,7 +30,7 @@ struct Root: Component { fileprivate var tokenList: TokenList fileprivate var modal: Modal fileprivate let deviceCanScan: Bool - fileprivate var auth: Auth + fileprivate var screenLock: ScreenLock fileprivate enum Modal { case none @@ -58,7 +58,7 @@ struct Root: Component { init(deviceCanScan: Bool, screenLockEnabled: Bool) { tokenList = TokenList() modal = .none - auth = Auth(screenLockEnabled: screenLockEnabled) + screenLock = ScreenLock(screenLockEnabled: screenLockEnabled) self.deviceCanScan = deviceCanScan } } @@ -77,7 +77,7 @@ extension Root { let viewModel = ViewModel( tokenList: tokenListViewModel, modal: modal.viewModel(digitGroupSize: digitGroupSize), - privacy: auth.viewModel + screenLock: screenLock.viewModel ) return (viewModel: viewModel, nextRefreshTime: nextRefreshTime) } @@ -92,9 +92,9 @@ extension Root { case tokenEditFormAction(TokenEditForm.Action) case tokenScannerAction(TokenScanner.Action) case menuAction(Menu.Action) + case screenLockAction(ScreenLock.Action) case addTokenFromURL(Token) - case authAction(Auth.Action) } enum Event { @@ -109,7 +109,7 @@ extension Root { case applicationDidBecomeActive case applicationWillResignActive - case authenticationEvent(Auth.Event) + case screenLockEvent(ScreenLock.Event) } enum Effect { @@ -177,8 +177,8 @@ extension Root { success: Event.addTokenFromURLSucceeded, failure: Event.addTokenFailed) - case .authAction(let action): - return try auth.update(with: action).flatMap { handleAuthEffect($0) } + case .screenLockAction(let action): + return try screenLock.update(with: action).flatMap { handleScreenLockEffect($0) } } } catch { throw ComponentError(underlyingError: error, action: action, component: self) @@ -207,21 +207,21 @@ extension Root { return .showErrorMessage("Failed to delete token.") case .applicationDidBecomeActive: - let effect = auth.update(with: .applicationDidBecomeActive) + let effect = screenLock.update(with: .applicationDidBecomeActive) return effect.flatMap { effect in - handleAuthEffect(effect) + handleScreenLockEffect(effect) } case .applicationWillResignActive: - let effect = auth.update(with: .applicationWillResignActive) + let effect = screenLock.update(with: .applicationWillResignActive) return effect.flatMap { effect in - handleAuthEffect(effect) + handleScreenLockEffect(effect) } - case .authenticationEvent(let authEvent): - let effect = auth.update(with: authEvent) + case .screenLockEvent(let screenLockEvent): + let effect = screenLock.update(with: screenLockEvent) return effect.flatMap { effect in - handleAuthEffect(effect) + handleScreenLockEffect(effect) } } } @@ -354,11 +354,11 @@ extension Root { } } - private mutating func handleAuthEffect(_ effect: Auth.Effect) -> Effect? { + private mutating func handleScreenLockEffect(_ effect: ScreenLock.Effect) -> Effect? { switch effect { case let .authenticateUser(success, failure): - return .authenticateUser(success: .authenticationEvent(success), - failure: { .authenticationEvent(failure($0)) }) + return .authenticateUser(success: .screenLockEvent(success), + failure: { .screenLockEvent(failure($0)) }) } } } diff --git a/Authenticator/Source/RootViewController.swift b/Authenticator/Source/RootViewController.swift index 9b9596b5..a9b3f290 100644 --- a/Authenticator/Source/RootViewController.swift +++ b/Authenticator/Source/RootViewController.swift @@ -67,7 +67,7 @@ class RootViewController: OpaqueNavigationController { super.init(nibName: nil, bundle: nil) self.viewControllers = [tokenListViewController] - updateWithAuthViewModel(viewModel.privacy) + updateWithScreenLockViewModel(viewModel.screenLock) } @available(*, unavailable) @@ -177,7 +177,7 @@ extension RootViewController { actionTransform: compose(Menu.Action.infoListEffect, Root.Action.menuAction)) } } - updateWithAuthViewModel(viewModel.privacy) + updateWithScreenLockViewModel(viewModel.screenLock) currentViewModel = viewModel } @@ -208,7 +208,7 @@ extension RootViewController { presentViewControllers([viewControllerA, viewControllerB]) } - private func updateWithAuthViewModel(_ viewModel: Auth.ViewModel) { + private func updateWithScreenLockViewModel(_ viewModel: ScreenLock.ViewModel) { if viewModel.enabled && authController?.presentingViewController == nil { if authController == nil { authController = UIViewController() @@ -241,7 +241,7 @@ extension RootViewController { @objc private func tryToUnlock() { - dispatchAction(.authAction(.tryToUnlock)) + dispatchAction(.screenLockAction(.tryToUnlock)) } } diff --git a/Authenticator/Source/RootViewModel.swift b/Authenticator/Source/RootViewModel.swift index f3419607..85b516ec 100644 --- a/Authenticator/Source/RootViewModel.swift +++ b/Authenticator/Source/RootViewModel.swift @@ -26,7 +26,7 @@ struct RootViewModel { let tokenList: TokenList.ViewModel let modal: ModalViewModel - let privacy: Auth.ViewModel + let screenLock: ScreenLock.ViewModel enum ModalViewModel { case none diff --git a/Authenticator/Source/Auth.swift b/Authenticator/Source/ScreenLock.swift similarity index 98% rename from Authenticator/Source/Auth.swift rename to Authenticator/Source/ScreenLock.swift index 2a86a9b6..39bab988 100644 --- a/Authenticator/Source/Auth.swift +++ b/Authenticator/Source/ScreenLock.swift @@ -1,5 +1,5 @@ // -// Auth.swift +// ScreenLock.swift // Authenticator // // Copyright (c) 2017-2018 Authenticator authors @@ -23,7 +23,7 @@ // SOFTWARE. // -struct Auth: Component { +struct ScreenLock: Component { private var state: State private enum State: Equatable { From a06a3f5b2a84835454f754cdc00e91189ebe3aa7 Mon Sep 17 00:00:00 2001 From: Matt Rubin Date: Thu, 14 Mar 2019 18:21:35 -0400 Subject: [PATCH 23/23] Refactor for a more future-proof handling of applicationDidBecomeActive --- Authenticator/Source/ScreenLock.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Authenticator/Source/ScreenLock.swift b/Authenticator/Source/ScreenLock.swift index 39bab988..4d221c25 100644 --- a/Authenticator/Source/ScreenLock.swift +++ b/Authenticator/Source/ScreenLock.swift @@ -87,10 +87,12 @@ struct ScreenLock: Component { mutating func update(with event: Event) -> Effect? { switch event { case .applicationDidBecomeActive: - if case .locked(shouldAuthenticateAutomatically: true) = state { + switch state { + case .locked(shouldAuthenticateAutomatically: true): return .authenticateUser(success: .authenticationSucceeded, failure: Event.authenticationFailed) - } else { + case .unlocked, + .locked(shouldAuthenticateAutomatically: false): return nil }