diff --git a/Authenticator.xcodeproj/project.pbxproj b/Authenticator.xcodeproj/project.pbxproj index ad03ca84..6f37c280 100644 --- a/Authenticator.xcodeproj/project.pbxproj +++ b/Authenticator.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 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 */; }; B452EC1F2A1E4F460045E5D9 /* YubiOtpRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B452EC1E2A1E4F460045E5D9 /* YubiOtpRowView.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 = ""; }; @@ -307,6 +309,7 @@ children = ( 5156D05C265D2602007A94F8 /* TokenRequestViewController.swift */, B4FE90D32A443D8400B59170 /* TokenRequestWrapper.swift */, + B40F44442B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift */, ); path = TokenSession; sourceTree = ""; @@ -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 */, diff --git a/Authenticator/Model/TokenRequestViewModel.swift b/Authenticator/Model/TokenRequestViewModel.swift index 9881280d..a86e3031 100644 --- a/Authenticator/Model/TokenRequestViewModel.swift +++ b/Authenticator/Model/TokenRequestViewModel.swift @@ -178,6 +178,77 @@ class TokenRequestViewModel: NSObject { } } + +extension TokenRequestViewModel { + + func isYubiOTPEnabledOverUSBC(completion: @escaping (Bool) -> Void) { + print(#function) + connection.smartCardConnection { connection in + print("got \(connection)") + connection?.managementSession { session, error in + print("got \(session)") + guard let session else { return } + session.getDeviceInfo { deviceInfo, error in + print("got \(deviceInfo)") + guard let deviceInfo, let configuration = deviceInfo.configuration else { return } + guard !configuration.isEnabled(.OTP, overTransport: .USB) || SettingsConfig.isOTPOverUSBIgnored(deviceId: deviceInfo.serialNumber + 1) else { + print("yubiotp enabled") + completion(true) + return + } + print("yubiotp disabled") + completion(false) + } + } + } + } + + func disableOTP(completion: @escaping (Error?) -> Void) { + print(#function) + connection.smartCardConnection { connection in + connection?.managementSession { session, error in + print(session) + guard let session else { return } + session.getDeviceInfo { deviceInfo, error in + print(deviceInfo) + guard let deviceInfo, let configuration = deviceInfo.configuration else { return } + configuration.setEnabled(false, application: .OTP, overTransport: .USB) + session.write(configuration, reboot: true) { error in + print(error) + completion(error) + } + } + } + } + } + + func waitForKeyRemoval(completion: @escaping () -> Void) { + print(#function) + connection.didDisconnect { _, _ in + print("") + completion() + } + } + + func ignoreThisKey(handler: @escaping (Error?) -> Void) { + print(#function) + connection.smartCardConnection { connection in + print(connection) + connection?.managementSession { session, error in + print(session) + guard let session else { handler(error); return } + session.getDeviceInfo { deviceInfo, error in + print(deviceInfo) + guard let deviceInfo else { handler(error); return } + SettingsConfig.registerUSBCDeviceToIgnore(deviceId: deviceInfo.serialNumber) + handler(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..36a8ec8e 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..e220edb3 100644 --- a/Authenticator/UI/TokenSession/TokenRequestViewController.swift +++ b/Authenticator/UI/TokenSession/TokenRequestViewController.swift @@ -68,22 +68,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 yubiOTPEnabled { + 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 +141,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..095f3c44 --- /dev/null +++ b/Authenticator/UI/TokenSession/TokenRequestYubiOTPViewController.swift @@ -0,0 +1,42 @@ +// +// 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! + + @IBAction func disableOTP() { + viewModel?.disableOTP { error in + guard error == nil else { return } + UIView.animate(withDuration: 0.5) { + DispatchQueue.main.async { + self.optionsView.alpha = 0 + self.completedView.alpha = 1 + self.viewModel?.waitForKeyRemoval { + self.dismiss(animated: true) + } + } + } + } + } + + @IBAction func ignoreThisKey() { + viewModel?.ignoreThisKey { error in + guard error == nil else { return } + DispatchQueue.main.async { + self.dismiss(animated: true) + } + } + } +}