diff --git a/Authenticator.xcodeproj/project.pbxproj b/Authenticator.xcodeproj/project.pbxproj index ad03ca84..fb588860 100644 --- a/Authenticator.xcodeproj/project.pbxproj +++ b/Authenticator.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -81,8 +81,9 @@ B40327762847AE0A00DF4DB0 /* Licensing.md in Resources */ = {isa = PBXBuildFile; fileRef = B40327752847AE0A00DF4DB0 /* Licensing.md */; }; B40D61A02AE7F37900467AE9 /* DisableOTPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40D619F2AE7F37900467AE9 /* DisableOTPView.swift */; }; B40D61A22AE7F89500467AE9 /* DisableOTPModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40D61A12AE7F89500467AE9 /* DisableOTPModel.swift */; }; + B40F44452B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40F44442B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift */; }; B411242F29D423A300D58001 /* ListStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B411242E29D423A300D58001 /* ListStatusView.swift */; }; - B432B1BF28B65B8600A7182F /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = B432B1BE28B65B8600A7182F /* YubiKit */; }; + B42A39332B2A03D20039DB26 /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = B42A39322B2A03D20039DB26 /* YubiKit */; }; B452EC1F2A1E4F460045E5D9 /* YubiOtpRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B452EC1E2A1E4F460045E5D9 /* YubiOtpRowView.swift */; }; B452EC3D2A264A620045E5D9 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B452EC3C2A264A620045E5D9 /* ToastView.swift */; }; B452EC442A2A06940045E5D9 /* ToastPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B452EC432A2A06940045E5D9 /* ToastPresenter.swift */; }; @@ -224,6 +225,7 @@ B40327752847AE0A00DF4DB0 /* Licensing.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Licensing.md; sourceTree = ""; }; B40D619F2AE7F37900467AE9 /* DisableOTPView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableOTPView.swift; sourceTree = ""; }; B40D61A12AE7F89500467AE9 /* DisableOTPModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableOTPModel.swift; sourceTree = ""; }; + B40F44442B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRequestYubiOTPViewController.swift; sourceTree = ""; }; B411242E29D423A300D58001 /* ListStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStatusView.swift; sourceTree = ""; }; B452EC1E2A1E4F460045E5D9 /* YubiOtpRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YubiOtpRowView.swift; sourceTree = ""; }; B452EC3C2A264A620045E5D9 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; @@ -263,7 +265,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B432B1BF28B65B8600A7182F /* YubiKit in Frameworks */, + B42A39332B2A03D20039DB26 /* YubiKit in Frameworks */, B9F0FF11F842A39183974083 /* (null) in Frameworks */, 51AFD4DA271D4278008F2630 /* QuartzCore.framework in Frameworks */, ); @@ -307,6 +309,7 @@ children = ( 5156D05C265D2602007A94F8 /* TokenRequestViewController.swift */, B4FE90D32A443D8400B59170 /* TokenRequestWrapper.swift */, + B40F44442B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift */, ); path = TokenSession; sourceTree = ""; @@ -581,7 +584,7 @@ ); name = Authenticator; packageProductDependencies = ( - B432B1BE28B65B8600A7182F /* YubiKit */, + B42A39322B2A03D20039DB26 /* YubiKit */, ); productName = Authenticator; productReference = 818866B322DFD729006BC0A8 /* Authenticator.app */; @@ -642,7 +645,7 @@ ); mainGroup = 818866AA22DFD729006BC0A8; packageReferences = ( - B432B1BD28B65B8600A7182F /* XCRemoteSwiftPackageReference "yubikit-ios" */, + B42A39312B2A03D20039DB26 /* XCLocalSwiftPackageReference "../yubikit-ios" */, ); productRefGroup = 818866B422DFD729006BC0A8 /* Products */; projectDirPath = ""; @@ -720,6 +723,7 @@ A525965B23A45501006AA3C0 /* UIImageAdditions.swift in Sources */, 51A162862678A1F100C3FA1E /* OATHConfigurationController.swift in Sources */, 515542622649C88900B19C59 /* PasswordConfigurationViewModel.swift in Sources */, + B40F44452B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift in Sources */, B4C93E60299D156C00C2A8B8 /* ErrorAlertView.swift in Sources */, A591411D23830EB800CCCF67 /* UIApplicationExtension.swift in Sources */, 81FA3C34231AF2D8009C22AB /* AdvancedSettingsViewController.swift in Sources */, @@ -1002,7 +1006,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Authenticator/Authenticator.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 121; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = LQA3CS5MM7; HEADER_SEARCH_PATHS = "../Submodules/YubiKit/**"; INFOPLIST_FILE = Authenticator/Info.plist; @@ -1012,7 +1016,7 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ""; - MARKETING_VERSION = 1.7.8; + MARKETING_VERSION = 1.7.9; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.yubico.Authenticator; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1030,7 +1034,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Authenticator/Authenticator.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 121; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = LQA3CS5MM7; HEADER_SEARCH_PATHS = "../Submodules/YubiKit/**"; INFOPLIST_FILE = Authenticator/Info.plist; @@ -1040,7 +1044,7 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ""; - MARKETING_VERSION = 1.7.8; + MARKETING_VERSION = 1.7.9; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.yubico.Authenticator; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1136,21 +1140,16 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - B432B1BD28B65B8600A7182F /* XCRemoteSwiftPackageReference "yubikit-ios" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Yubico/yubikit-ios"; - requirement = { - branch = main; - kind = branch; - }; +/* Begin XCLocalSwiftPackageReference section */ + B42A39312B2A03D20039DB26 /* XCLocalSwiftPackageReference "../yubikit-ios" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../yubikit-ios"; }; -/* End XCRemoteSwiftPackageReference section */ +/* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - B432B1BE28B65B8600A7182F /* YubiKit */ = { + B42A39322B2A03D20039DB26 /* YubiKit */ = { isa = XCSwiftPackageProductDependency; - package = B432B1BD28B65B8600A7182F /* XCRemoteSwiftPackageReference "yubikit-ios" */; productName = YubiKit; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Authenticator/Model/TokenRequestViewModel.swift b/Authenticator/Model/TokenRequestViewModel.swift index 9881280d..4d0aa867 100644 --- a/Authenticator/Model/TokenRequestViewModel.swift +++ b/Authenticator/Model/TokenRequestViewModel.swift @@ -74,7 +74,8 @@ class TokenRequestViewModel: NSObject { } var isWiredKeyConnectedHandler: ((Bool) -> Void)? - + var isYubiOTPEnabledHandler: ((Bool) -> Void)? + func isWiredKeyConnected(handler: @escaping (Bool) -> Void) { isWiredKeyConnectedHandler = handler connection.smartCardConnection { [weak self] connection in @@ -82,9 +83,11 @@ class TokenRequestViewModel: NSObject { self?.isWiredKeyConnectedHandler?(connection != nil) } } - connection.accessoryConnection { [weak self] connection in - DispatchQueue.main.async { - self?.isWiredKeyConnectedHandler?(connection != nil) + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + connection.accessoryConnection { [weak self] connection in + DispatchQueue.main.async { + self?.isWiredKeyConnectedHandler?(connection != nil) + } } } } @@ -178,6 +181,67 @@ class TokenRequestViewModel: NSObject { } } + +extension TokenRequestViewModel { + + func isYubiOTPEnabledOverUSBC(completion: @escaping (Bool?) -> Void) { + isYubiOTPEnabledHandler = completion + + // If this device does not have a lightning port return nil + if YubiKitDeviceCapabilities.supportsMFIAccessoryKey { + completion(nil) + return + } + connection.smartCardConnection { [weak self] connection in + connection?.managementSession { session, error in + guard let session else { self?.isYubiOTPEnabledHandler?(false); return } + session.getDeviceInfo { deviceInfo, error in + guard let deviceInfo, let configuration = deviceInfo.configuration else { self?.isYubiOTPEnabledHandler?(false); return } + guard !configuration.isEnabled(.OTP, overTransport: .USB) || SettingsConfig.isOTPOverUSBIgnored(deviceId: deviceInfo.serialNumber) else { + self?.isYubiOTPEnabledHandler?(true) + return + } + self?.isYubiOTPEnabledHandler?(false) + } + } + } + } + + func disableOTP(completion: @escaping (Error?) -> Void) { + connection.smartCardConnection { connection in + connection?.managementSession { session, error in + guard let session else { completion(error); return } + session.getDeviceInfo { deviceInfo, error in + guard let deviceInfo, let configuration = deviceInfo.configuration else { completion(error); return } + configuration.setEnabled(false, application: .OTP, overTransport: .USB) + session.write(configuration, reboot: true) { error in + completion(error) + } + } + } + } + } + + func waitForKeyRemoval(completion: @escaping () -> Void) { + connection.didDisconnect { _, _ in + completion() + } + } + + func ignoreThisKey(completion: @escaping (Error?) -> Void) { + connection.smartCardConnection { connection in + connection?.managementSession { session, error in + guard let session else { completion(error); return } + session.getDeviceInfo { deviceInfo, error in + guard let deviceInfo else { completion(error); return } + SettingsConfig.registerUSBCDeviceToIgnore(deviceId: deviceInfo.serialNumber) + completion(nil) + } + } + } + } +} + @available(iOS 14.0, *) private extension YKFPIVSession { func slotForObjectId(_ objectId: String, completion: @escaping (YKFPIVSlot?, TokenRequestViewModel.TokenError?) -> Void) { diff --git a/Authenticator/UI/Base.lproj/Main.storyboard b/Authenticator/UI/Base.lproj/Main.storyboard index df39c8db..72bd3467 100644 --- a/Authenticator/UI/Base.lproj/Main.storyboard +++ b/Authenticator/UI/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -1197,7 +1197,7 @@ All rights reserved. - + @@ -1535,6 +1535,103 @@ All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1558,7 +1655,7 @@ All rights reserved. - + diff --git a/Authenticator/UI/TokenSession/TokenRequestViewController.swift b/Authenticator/UI/TokenSession/TokenRequestViewController.swift index 715e821d..3f47c548 100644 --- a/Authenticator/UI/TokenSession/TokenRequestViewController.swift +++ b/Authenticator/UI/TokenSession/TokenRequestViewController.swift @@ -39,6 +39,10 @@ class TokenRequestViewController: UIViewController, UITextFieldDelegate { override func viewDidLoad() { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) defaultAccessoryTest = accessoryLabel.text overlayView.alpha = 0 arrowHintView.alpha = 0 @@ -48,11 +52,11 @@ class TokenRequestViewController: UIViewController, UITextFieldDelegate { if !YubiKitDeviceCapabilities.supportsISO7816NFCTags { orView.alpha = 0 nfcView.alpha = 0 + } else { + orView.alpha = 1 + nfcView.alpha = 1 } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + // We need to tighten the vertical spacing to make room for the keyboard on smaller screens if UIScreen.main.bounds.width <= 320 { topHeadingConstraint.constant = 40 @@ -68,22 +72,35 @@ class TokenRequestViewController: UIViewController, UITextFieldDelegate { viewModel = TokenRequestViewModel() passwordTextField.becomeFirstResponder() passwordTextField.delegate = self - viewModel?.isWiredKeyConnected { [weak self] connected in - if !connected && self?.orView.alpha == 1 { return } - UIView.animate(withDuration: 0.2) { - self?.orView.alpha = 0 - self?.nfcView.alpha = 0 - self?.accessoryLabel.alpha = 0 - } completion: { _ in + + viewModel?.isYubiOTPEnabledOverUSBC { yubiOTPEnabled in + if let yubiOTPEnabled, yubiOTPEnabled == true { + DispatchQueue.main.async { + let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let vc: TokenRequestYubiOTPViewController = storyboard.instantiateViewController(withIdentifier: "TokenRequestYubiOTPViewController") as! TokenRequestYubiOTPViewController + vc.viewModel = self.viewModel + vc.modalPresentationStyle = .fullScreen + self.present(vc, animated: true) + } + } + + self.viewModel?.isWiredKeyConnected { [weak self] connected in + if !connected && self?.orView.alpha == 1 { return } UIView.animate(withDuration: 0.2) { - self?.accessoryLabel.alpha = 1 - if connected { - self?.accessoryLabel.text = "Enter the PIN to access the certificate." - } else { - self?.accessoryLabel.text = self?.defaultAccessoryTest - if YubiKitDeviceCapabilities.supportsISO7816NFCTags { - self?.orView.alpha = 1 - self?.nfcView.alpha = 1 + self?.orView.alpha = 0 + self?.nfcView.alpha = 0 + self?.accessoryLabel.alpha = 0 + } completion: { _ in + UIView.animate(withDuration: 0.2) { + self?.accessoryLabel.alpha = 1 + if connected { + self?.accessoryLabel.text = "Enter the PIN to access the certificate." + } else { + self?.accessoryLabel.text = self?.defaultAccessoryTest + if YubiKitDeviceCapabilities.supportsISO7816NFCTags { + self?.orView.alpha = 1 + self?.nfcView.alpha = 1 + } } } } @@ -128,7 +145,6 @@ class TokenRequestViewController: UIViewController, UITextFieldDelegate { deinit { NotificationCenter.default.removeObserver(self) - print("Deinit TokenRequestViewController") } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { diff --git a/Authenticator/UI/TokenSession/TokenRequestYubiOTPViewController.swift b/Authenticator/UI/TokenSession/TokenRequestYubiOTPViewController.swift new file mode 100644 index 00000000..f243fbd9 --- /dev/null +++ b/Authenticator/UI/TokenSession/TokenRequestYubiOTPViewController.swift @@ -0,0 +1,58 @@ +// +// TokenRequestYubiOTP.swift +// Authenticator +// +// Created by Jens Utbult on 2023-12-11. +// Copyright © 2023 Yubico. All rights reserved. +// + +import Foundation + +@available(iOS 14.0, *) +class TokenRequestYubiOTPViewController: UIViewController { + + var viewModel: TokenRequestViewModel? + + @IBOutlet weak var optionsView: UIStackView! + @IBOutlet weak var completedView: UIStackView! + + + override func viewDidLoad() { + self.viewModel?.waitForKeyRemoval { [weak self] in + self?.dismiss(animated: true) + } + } + + @IBAction func disableOTP() { + viewModel?.disableOTP { error in + guard error == nil else { + self.presentError(error) + return + } + DispatchQueue.main.async { + UIView.animate(withDuration: 0.5) { + self.optionsView.alpha = 0 + self.completedView.alpha = 1 + } + } + } + } + + @IBAction func ignoreThisKey() { + viewModel?.ignoreThisKey { error in + guard error == nil else { + self.presentError(error) + return + } + DispatchQueue.main.async { + self.dismiss(animated: true) + } + } + } + + private func presentError(_ error: Error?) { + guard let error else { return } + let alert = UIAlertController(title: "Error reading YubiKey", message: "\(error.localizedDescription)\n\nRemove and reinsert your YubiKey.") { self.dismiss(animated: true) } + self.present(alert, animated: true, completion: nil) + } +} diff --git a/Authenticator/VersionHistory.plist b/Authenticator/VersionHistory.plist index 39bae137..fe10859e 100644 --- a/Authenticator/VersionHistory.plist +++ b/Authenticator/VersionHistory.plist @@ -2,7 +2,18 @@ - + + version + 1.7.9 + date + 2023-12-20T09:41:00Z + shouldPromptUser + + changes + + This version solves a bug that caused a "Credential not found" error to be displayed instead of a list of accounts. Improved handling on iPhone 15 with YubiKeys that have Yubico OTP enabled when using the SmartCard extension. + + version 1.7.8