From c6dda52a7e467a2c443ea0026f5de48c94c08a94 Mon Sep 17 00:00:00 2001 From: Jens Utbult Date: Thu, 29 Aug 2024 14:20:19 +0200 Subject: [PATCH] Ported set/change/remove OATH password to SwiftUI. --- Authenticator.xcodeproj/project.pbxproj | 14 ++ Authenticator/Localizable.xcstrings | 49 +++++ .../Model/OATHPasswordViewModel.swift | 158 +++++++++++++++++ Authenticator/UI/Base.lproj/Main.storyboard | 26 ++- Authenticator/UI/Helpers/SettingsButton.swift | 60 +++++++ Authenticator/UI/MainView.swift | 2 +- .../ConfigurationController.swift | 6 + .../ConfigurationWrapper.swift | 4 +- .../NFCSettingsController.swift | 2 +- .../OATHPasswordView.swift | 167 ++++++++++++++++++ .../YubiKeyConfiguration/OATHResetView.swift | 13 +- 11 files changed, 479 insertions(+), 22 deletions(-) create mode 100644 Authenticator/Model/OATHPasswordViewModel.swift create mode 100644 Authenticator/UI/Helpers/SettingsButton.swift create mode 100644 Authenticator/UI/YubiKeyConfiguration/OATHPasswordView.swift diff --git a/Authenticator.xcodeproj/project.pbxproj b/Authenticator.xcodeproj/project.pbxproj index 95e7cc71..47f5f9f8 100644 --- a/Authenticator.xcodeproj/project.pbxproj +++ b/Authenticator.xcodeproj/project.pbxproj @@ -82,6 +82,8 @@ B42A39332B2A03D20039DB26 /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = B42A39322B2A03D20039DB26 /* YubiKit */; }; B44E5E812C74BE67007ABB79 /* OATHResetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44E5E802C74BE67007ABB79 /* OATHResetView.swift */; }; B44E5E842C74C8CC007ABB79 /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = B44E5E832C74C8CC007ABB79 /* YubiKit */; }; + B44E5EB32C777F22007ABB79 /* OATHPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44E5EB22C777F22007ABB79 /* OATHPasswordViewModel.swift */; }; + B44E5EBB2C778802007ABB79 /* OATHPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44E5EBA2C778802007ABB79 /* OATHPasswordView.swift */; }; 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 */; }; @@ -100,6 +102,8 @@ B4901AF42BD7DF960092E7A2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4901AF32BD7DF960092E7A2 /* Localizable.xcstrings */; }; B4901B082BD91DA60092E7A2 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4901B072BD91DA60092E7A2 /* InfoPlist.xcstrings */; }; B4B1711827DF8C48002A62DE /* ScanAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1711727DF8C48002A62DE /* ScanAccountView.swift */; }; + B4BB02D92C80987B00B72904 /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BB02D82C80987B00B72904 /* SettingsButton.swift */; }; + B4BB02DA2C809BE800B72904 /* NFCSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AFD4D32716FC78008F2630 /* NFCSettingsController.swift */; }; B4C93E60299D156C00C2A8B8 /* ErrorAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C93E5F299D156C00C2A8B8 /* ErrorAlertView.swift */; }; B4C93E63299FB51A00C2A8B8 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C93E62299FB51A00C2A8B8 /* Account.swift */; }; B4C93E65299FC67800C2A8B8 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C93E64299FC67800C2A8B8 /* View+Extensions.swift */; }; @@ -230,6 +234,8 @@ B40F44442B27033A000D5E02 /* TokenRequestYubiOTPViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRequestYubiOTPViewController.swift; sourceTree = ""; }; B411242E29D423A300D58001 /* ListStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStatusView.swift; sourceTree = ""; }; B44E5E802C74BE67007ABB79 /* OATHResetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATHResetView.swift; sourceTree = ""; }; + B44E5EB22C777F22007ABB79 /* OATHPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATHPasswordViewModel.swift; sourceTree = ""; }; + B44E5EBA2C778802007ABB79 /* OATHPasswordView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OATHPasswordView.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 = ""; }; B452EC432A2A06940045E5D9 /* ToastPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastPresenter.swift; sourceTree = ""; }; @@ -256,6 +262,7 @@ B4901AF32BD7DF960092E7A2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; B4901B072BD91DA60092E7A2 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; B4B1711727DF8C48002A62DE /* ScanAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanAccountView.swift; sourceTree = ""; }; + B4BB02D82C80987B00B72904 /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; }; B4C93E5F299D156C00C2A8B8 /* ErrorAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertView.swift; sourceTree = ""; }; B4C93E62299FB51A00C2A8B8 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; B4C93E64299FC67800C2A8B8 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; @@ -312,6 +319,7 @@ 513D4DF02660D5AB0022C53D /* YubiKeyConfiguration */ = { isa = PBXGroup; children = ( + B44E5EBA2C778802007ABB79 /* OATHPasswordView.swift */, B4C93E9029C0B70B00C2A8B8 /* ConfigurationWrapper.swift */, 513D4DF42660EBA40022C53D /* ConfigurationController.swift */, 51A162852678A1F100C3FA1E /* OATHConfigurationController.swift */, @@ -421,6 +429,7 @@ B4C93E8829B89DE300C2A8B8 /* DetachedMenu.swift */, B452EC3C2A264A620045E5D9 /* ToastView.swift */, B452EC432A2A06940045E5D9 /* ToastPresenter.swift */, + B4BB02D82C80987B00B72904 /* SettingsButton.swift */, ); path = Helpers; sourceTree = ""; @@ -519,6 +528,7 @@ 513D4DF8266A21250022C53D /* DelegateStack.swift */, 51002C2D267C95D9005D5A7C /* YubiKeyInformationViewModel.swift */, 5180974226DE185100A122C1 /* ResetOATHViewModel.swift */, + B44E5EB22C777F22007ABB79 /* OATHPasswordViewModel.swift */, 51AFD4D52716FCDB008F2630 /* ApplicationSettingsViewModel.swift */, A5E9DEAF237DE1660011FBF4 /* SettingsConfig.swift */, B4DB2289299BC373003110ED /* OATHSession.swift */, @@ -741,6 +751,7 @@ A591412123835F4600CCCF67 /* Constants.swift in Sources */, 515542852656A30B00B19C59 /* UIButton+Extensions.swift in Sources */, 51BBE37F273982D700DA47CC /* YKFOATHSession+Extensions.swift in Sources */, + B44E5EB32C777F22007ABB79 /* OATHPasswordViewModel.swift in Sources */, B44E5E812C74BE67007ABB79 /* OATHResetView.swift in Sources */, B411242F29D423A300D58001 /* ListStatusView.swift in Sources */, 816C684823430F8E00209342 /* SecureStoreQueryable.swift in Sources */, @@ -783,6 +794,7 @@ 51D1E84E26427F7600BDA3FF /* PasswordCache.swift in Sources */, A5D4E86D24083CF300FD63A0 /* OTPConfigurationController.swift in Sources */, 5156D05F265D3CEF007A94F8 /* TokenRequestViewModel.swift in Sources */, + B4BB02D92C80987B00B72904 /* SettingsButton.swift in Sources */, 5155426A26554CAB00B19C59 /* SmartCardViewModel.swift in Sources */, B4C93E9329C1B2BC00C2A8B8 /* AboutWrapper.swift in Sources */, 515542682654413600B19C59 /* SmartCardConfigurationController.swift in Sources */, @@ -798,6 +810,7 @@ 5155428A2656F79500B19C59 /* SecCertificate+Extensions.swift in Sources */, 816C685E234697BF00209342 /* PasswordPreferences.swift in Sources */, B4C93E9529C1B90900C2A8B8 /* AddAccountWrapper.swift in Sources */, + B44E5EBB2C778802007ABB79 /* OATHPasswordView.swift in Sources */, B452EC3D2A264A620045E5D9 /* ToastView.swift in Sources */, 513D4DF7266634280022C53D /* ShowTime.swift in Sources */, 513D4DF52660EBA40022C53D /* ConfigurationController.swift in Sources */, @@ -808,6 +821,7 @@ 51EEC532246D34ED00061A8F /* YKFKeyVersion+Extensions.swift in Sources */, B4719B1B298AB641006CDAEA /* MainViewModel.swift in Sources */, 51394C5826CFE15F009F366D /* YubiMenu.swift in Sources */, + B4BB02DA2C809BE800B72904 /* NFCSettingsController.swift in Sources */, A5B531F22437CD16008C501C /* VersionHistoryViewController.swift in Sources */, B4C93E65299FC67800C2A8B8 /* View+Extensions.swift in Sources */, 816C685023440D2200209342 /* SecureStoreError.swift in Sources */, diff --git a/Authenticator/Localizable.xcstrings b/Authenticator/Localizable.xcstrings index 9cd286a9..40c5153d 100644 --- a/Authenticator/Localizable.xcstrings +++ b/Authenticator/Localizable.xcstrings @@ -321,6 +321,12 @@ } } } + }, + "Change password" : { + + }, + "Change the password for this YubiKey. %@" : { + }, "Clear" : { "comment" : "Clear password alert button", @@ -474,6 +480,9 @@ } } } + }, + "Confirm OATH reset" : { + }, "Continue with limited usability" : { "localizations" : { @@ -524,6 +533,9 @@ } } } + }, + "Current password" : { + }, "Data to String conversion error" : { "localizations" : { @@ -827,6 +839,7 @@ }, "Failed to reset YubiKey" : { "comment" : "Reset YubiKey failure alert title", + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -991,6 +1004,9 @@ } } } + }, + "Manage OATH passwords" : { + }, "Menu" : { "localizations" : { @@ -1041,6 +1057,9 @@ } } } + }, + "New Password" : { + }, "No account information found!" : { "comment" : "Scan QR code no account error message", @@ -1260,6 +1279,9 @@ } } } + }, + "OATH passwords" : { + }, "Ok" : { "comment" : "Password alert", @@ -1562,6 +1584,9 @@ } } } + }, + "Protect this YubiKey with a password." : { + }, "Public key certificates on" : { "comment" : "PIV extension no certs on device message", @@ -1680,6 +1705,9 @@ } } } + }, + "Remove the password for this YubiKey." : { + }, "Rename" : { "comment" : "Menu", @@ -1697,6 +1725,9 @@ } } } + }, + "Repeat new password" : { + }, "Reset" : { "comment" : "Reset YubiKey alert button", @@ -1714,9 +1745,13 @@ } } } + }, + "Reset all accounts stored on YubiKey, make sure they are not in use anywhere before doing this." : { + }, "Reset complete" : { "comment" : "Reset YubiKey complete alert title", + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -1731,9 +1766,16 @@ } } } + }, + "Reset OATH" : { + + }, + "Reset OATH application" : { + }, "Reset YubiKey?" : { "comment" : "Reset YubiKey alert title", + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -1898,6 +1940,9 @@ } } } + }, + "Set password" : { + }, "Settings" : { "comment" : "PIV extension settings alert title", @@ -2256,6 +2301,7 @@ }, "This will delete all accounts and restore factory defaults of your YubiKey." : { "comment" : "Reset YubiKey alert message", + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -2638,6 +2684,9 @@ } } } + }, + "YubiKey has been reset" : { + }, "YubiKey information read" : { "comment" : "YubiKey info NFC read", diff --git a/Authenticator/Model/OATHPasswordViewModel.swift b/Authenticator/Model/OATHPasswordViewModel.swift new file mode 100644 index 00000000..9493fb5e --- /dev/null +++ b/Authenticator/Model/OATHPasswordViewModel.swift @@ -0,0 +1,158 @@ +// +// OATHPasswordViewModel.swift +// Authenticator +// +// Created by Jens Utbult on 2024-08-22. +// Copyright © 2024 Yubico. All rights reserved. +// + +class OATHPasswordViewModel: ObservableObject { + + @Published var state: PasswordState = .unknown + @Published var invalidPassword: Bool = false + @Published var isProcessing: Bool = false + + enum PasswordState: Equatable { + + case unknown, notSet, set, error(Error) + + static func == (lhs: OATHPasswordViewModel.PasswordState, rhs: OATHPasswordViewModel.PasswordState) -> Bool { + switch (lhs, rhs) { + case (.unknown, .unknown): + return true + case (.notSet, .notSet): + return true + case (.set, .set): + return true + case (.error(_), .error(_)): + return true + default: + return false + } + } + + func isError() -> Bool { + switch self { + case (.error(_)): + return true + default: + return false + } + } + } + + private let connection = Connection() + + init() { + connection.startConnection { connection in + connection.oathSession { session, error in + guard let session else { + YubiKitManager.shared.stopNFCConnection(withErrorMessage: error!.localizedDescription) + DispatchQueue.main.async { + self.state = .error(error!) + } + return + } + session.listCredentials { _, error in + DispatchQueue.main.async { + defer { YubiKitManager.shared.stopNFCConnection(withMessage: "Password state read") } + guard let error = error else { + self.state = .notSet + return + } + if let oathError = error as? YKFOATHError, oathError.code == YKFOATHErrorCode.authenticationRequired.rawValue { + self.state = .set + } else { + self.state = .error(error) + } + } + } + } + } + } + + func setPassword(_ password: String) { + self.isProcessing = true + connection.startConnection { connection in + connection.oathSession { session, error in + guard let session else { + DispatchQueue.main.async { + self.isProcessing = false + self.state = .error(error!) // If there is no error and no session crashing is the best thing. + } + YubiKitManager.shared.stopNFCConnection(withErrorMessage: error!.localizedDescription) + return + } + session.setPassword(password) { error in + DispatchQueue.main.async { + if let error { + self.state = .error(error) + YubiKitManager.shared.stopNFCConnection(withErrorMessage: error.localizedDescription) + } else { + self.state = .set + YubiKitManager.shared.stopNFCConnection(withMessage: "Password has been set") + } + self.isProcessing = false + } + } + } + } + } + + func changePassword(old oldPassword: String, new newPassword: String?) { + self.invalidPassword = false + self.isProcessing = true + connection.startConnection { connection in + connection.oathSession { session, error in + guard let session else { + DispatchQueue.main.async { + self.isProcessing = false + self.state = .error(error!) // If there is no error and no session crashing is the best thing. + } + YubiKitManager.shared.stopNFCConnection(withErrorMessage: error!.localizedDescription) + return + } + session.unlock(withPassword: oldPassword) { error in + if let error { + DispatchQueue.main.async { + if error.isInvalidPasswordError { + self.invalidPassword = true + } else { + self.state = .error(error) + } + self.isProcessing = false + } + YubiKitManager.shared.stopNFCConnection(withErrorMessage: error.localizedDescription) + return + } + session.setPassword(newPassword ?? "") { error in + DispatchQueue.main.async { + if let error { + self.state = .error(error) + YubiKitManager.shared.stopNFCConnection(withErrorMessage: error.localizedDescription) + } else { + self.state = newPassword != nil ? .set : .notSet + YubiKitManager.shared.stopNFCConnection(withMessage: newPassword != nil ? "Password has been changed" : "Password has been removed") + } + self.isProcessing = false + } + } + } + } + } + } + + func removePassword(current: String) { + changePassword(old: current, new: nil) + } +} + +extension Error { + var isInvalidPasswordError: Bool { + if let oathError = self as? YKFOATHError, oathError.code == YKFOATHErrorCode.wrongPassword.rawValue { + return true + } else { + return false + } + } +} diff --git a/Authenticator/UI/Base.lproj/Main.storyboard b/Authenticator/UI/Base.lproj/Main.storyboard index 367c0861..3f58c4de 100644 --- a/Authenticator/UI/Base.lproj/Main.storyboard +++ b/Authenticator/UI/Base.lproj/Main.storyboard @@ -430,7 +430,7 @@ All rights reserved. - + + + + @@ -936,7 +939,7 @@ All rights reserved. - + @@ -1419,7 +1422,7 @@ All rights reserved. - + @@ -1708,6 +1711,17 @@ All rights reserved. + + + + + + + + + + + @@ -1734,16 +1748,16 @@ All rights reserved. - + - + - + diff --git a/Authenticator/UI/Helpers/SettingsButton.swift b/Authenticator/UI/Helpers/SettingsButton.swift new file mode 100644 index 00000000..6b1afa3f --- /dev/null +++ b/Authenticator/UI/Helpers/SettingsButton.swift @@ -0,0 +1,60 @@ +/* + * Copyright (C) Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import SwiftUI + +public struct SettingsButton: View { + @Environment(\.isEnabled) var isEnabled + @Environment(\.displayScale) var displayScale + + private let text: String + private let action: () -> Void + + public init(_ text: String, action: @escaping () -> Void) { + self.text = text + self.action = action + } + + public var body: some View { + Button { + action() + } label: { + VStack(spacing: 0) { + Color(.separator) + .frame(height: 1.0 / displayScale) + .frame(maxWidth: .infinity) + .padding(0) + Text(text) + .font(.body) + .frame(maxWidth: .infinity) + .padding(12) + } + } + .buttonStyle(SettingsButtonStyle(isEnabled: isEnabled)) + } +} + +private struct SettingsButtonStyle: ButtonStyle { + let isEnabled: Bool + + @ViewBuilder + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background(configuration.isPressed ? Color(UIColor.systemGray4) : Color(.secondarySystemGroupedBackground)) + .foregroundStyle(isEnabled ? .blue : Color(.secondaryText)) + } +} diff --git a/Authenticator/UI/MainView.swift b/Authenticator/UI/MainView.swift index 46a2e957..b1feb03e 100644 --- a/Authenticator/UI/MainView.swift +++ b/Authenticator/UI/MainView.swift @@ -38,7 +38,7 @@ struct MainView: View { var insertYubiKeyMessage = { if YubiKitDeviceCapabilities.supportsISO7816NFCTags { - String(localized: "Insert YubiKey") + "\(!UIAccessibility.isVoiceOverRunning ? String(localized: "or pull down to activate NFC") : String(localized: "or scan a NFC YubiKey"))" + String(localized: "Insert YubiKey") + " \(!UIAccessibility.isVoiceOverRunning ? String(localized: "or pull down to activate NFC") : String(localized: "or scan a NFC YubiKey"))" } else { String(localized: "Insert YubiKey") } diff --git a/Authenticator/UI/YubiKeyConfiguration/ConfigurationController.swift b/Authenticator/UI/YubiKeyConfiguration/ConfigurationController.swift index 5397db5d..2d12fd02 100644 --- a/Authenticator/UI/YubiKeyConfiguration/ConfigurationController.swift +++ b/Authenticator/UI/YubiKeyConfiguration/ConfigurationController.swift @@ -125,6 +125,12 @@ extension ConfigurationController { controller?.title = "Reset OATH" return controller } + + @IBSegueAction func showOATHPasswordView(_ coder: NSCoder) -> UIViewController? { + let controller = UIHostingController(coder: coder, rootView: OATHPasswordView()) + controller?.title = "OATH password" + return controller + } } extension YKFManagementDeviceInfo { diff --git a/Authenticator/UI/YubiKeyConfiguration/ConfigurationWrapper.swift b/Authenticator/UI/YubiKeyConfiguration/ConfigurationWrapper.swift index 4589bfc0..dd5684f4 100644 --- a/Authenticator/UI/YubiKeyConfiguration/ConfigurationWrapper.swift +++ b/Authenticator/UI/YubiKeyConfiguration/ConfigurationWrapper.swift @@ -15,8 +15,8 @@ struct ConfigurationView: View { let navigationBarAppearance = UINavigationBarAppearance() init(showConfiguration: Binding ) { - navigationBarAppearance.shadowColor = .secondarySystemBackground - navigationBarAppearance.backgroundColor = .secondarySystemBackground + navigationBarAppearance.shadowColor = .systemGroupedBackground + navigationBarAppearance.backgroundColor = .systemGroupedBackground UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance _showConfiguration = showConfiguration } diff --git a/Authenticator/UI/YubiKeyConfiguration/NFCSettingsController.swift b/Authenticator/UI/YubiKeyConfiguration/NFCSettingsController.swift index c8958407..2355adea 100644 --- a/Authenticator/UI/YubiKeyConfiguration/NFCSettingsController.swift +++ b/Authenticator/UI/YubiKeyConfiguration/NFCSettingsController.swift @@ -54,6 +54,6 @@ class NFCSettingsController: UITableViewController { } deinit { - print("deinit ApplicationSettingsController") + print("deinit NFCSettingsController") } } diff --git a/Authenticator/UI/YubiKeyConfiguration/OATHPasswordView.swift b/Authenticator/UI/YubiKeyConfiguration/OATHPasswordView.swift new file mode 100644 index 00000000..cd6b4987 --- /dev/null +++ b/Authenticator/UI/YubiKeyConfiguration/OATHPasswordView.swift @@ -0,0 +1,167 @@ +// +// OATHPasswordView.swift +// Authenticator +// +// Created by Jens Utbult on 2024-08-22. +// Copyright © 2024 Yubico. All rights reserved. +// + + +import SwiftUI + +struct OATHPasswordView: View { + + @StateObject var model = OATHPasswordViewModel() + @Environment(\.dismiss) private var dismiss + + @State var presentSetPassword = false + @State var presentChangePassword = false + @State var presentRemovePassword = false + @State var presentErrorAlert = false + @State var errorMessage: String? = nil + + @State var showSetButton = true + @State var showChangeButton = false + @State var showRemoveButton = false + + @State var password: String = "" + @State var newPassword: String = "" + @State var repeatedPassword: String = "" + + func areButtonsDisabled() -> Bool { + model.state == .unknown || model.state.isError() || model.isProcessing + } + + func clearPasswords() { + password = "" + newPassword = "" + repeatedPassword = "" + } + + var body: some View { + VStack { + VStack(spacing: 0) { + VStack(spacing: 10) { + Image(systemName: "key") + .font(.system(size:50.0)) + .bold() + .foregroundColor(Color(.yubiBlue)) + .rotationEffect(Angle(degrees: 90)) + .accessibilityHidden(true) + Text("Manage OATH passwords").font(.headline) + Text("Reset all accounts stored on YubiKey, make sure they are not in use anywhere before doing this.") + .font(.callout) + .multilineTextAlignment(.center) + } + .padding(.horizontal, 30) + .padding(.bottom, 30) + if showSetButton { + SettingsButton("Set password") { + presentSetPassword.toggle() + }.disabled(areButtonsDisabled()) + } + if showChangeButton { + SettingsButton("Change password") { + presentChangePassword.toggle() + }.disabled(areButtonsDisabled()) + } + if showRemoveButton { + SettingsButton("Remove password") { + presentRemovePassword.toggle() + }.disabled(areButtonsDisabled()) + } + } + .frame(maxWidth: .infinity) + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 15, style: .continuous)) + .padding(20) + .opacity(model.state.isError() ? 0.5 : 1.0) + Spacer() + } + .background(Color(.systemGroupedBackground)) + .navigationBarTitle(Text("OATH passwords"), displayMode: .inline) + + .alert("Set password", isPresented: $presentSetPassword) { + SecureField("Password", text: $newPassword) + SecureField("Repeat new password", text: $repeatedPassword) + Button("OK") { + guard newPassword == repeatedPassword else { + errorMessage = "Passwords do not match" + presentErrorAlert = true + clearPasswords() + return + } + model.setPassword(newPassword) + clearPasswords() + } + } message: { + Text("Protect this YubiKey with a password.") + } + .alert("Change password", isPresented: $presentChangePassword) { + SecureField("Current password", text: $password) + SecureField("New Password", text: $newPassword) + SecureField("Repeat new password", text: $repeatedPassword) + Button("OK") { + guard newPassword == repeatedPassword else { + errorMessage = "New passwords do not match" + presentErrorAlert = true + clearPasswords() + return + } + model.changePassword(old: password, new: newPassword) + clearPasswords() + } + Button("Cancel", role: .cancel, action: { clearPasswords() }) + } message: { + Text("Change the password for this YubiKey. \(newPassword)") + } + .alert("Remove password", isPresented: $presentRemovePassword) { + SecureField("Current password", text: $password) + Button("OK", role: .destructive) { + model.removePassword(current: password) + clearPasswords() + presentRemovePassword.toggle() + } + Button("Cancel", role: .cancel, action: { clearPasswords() }) + } message: { + Text("Remove the password for this YubiKey.") + } + .alert(errorMessage ?? "Unknown error", isPresented: $presentErrorAlert, actions: { + Button(role: .cancel) { + errorMessage = nil + if model.state.isError() { + dismiss() + } + } label: { + Text("OK") + } + }) + .onChange(of: model.state) { state in + withAnimation { + switch state { + case .unknown: + self.showSetButton = true + self.showChangeButton = false + self.showRemoveButton = false + case .notSet: + self.showSetButton = true + self.showChangeButton = false + self.showRemoveButton = false + case .set: + self.showSetButton = false + self.showChangeButton = true + self.showRemoveButton = true + case .error(let error): + presentErrorAlert = true + self.errorMessage = error.localizedDescription + } + } + } + .onChange(of: model.invalidPassword) { invalidPassword in + if invalidPassword { + self.errorMessage = "Wrong password" + self.presentErrorAlert = true + } + } + } +} diff --git a/Authenticator/UI/YubiKeyConfiguration/OATHResetView.swift b/Authenticator/UI/YubiKeyConfiguration/OATHResetView.swift index c867b878..40258276 100644 --- a/Authenticator/UI/YubiKeyConfiguration/OATHResetView.swift +++ b/Authenticator/UI/YubiKeyConfiguration/OATHResetView.swift @@ -7,7 +7,6 @@ // import SwiftUI -import UIKit struct OATHResetView: View { @@ -30,18 +29,8 @@ struct OATHResetView: View { Text("Reset all accounts stored on YubiKey, make sure they are not in use anywhere before doing this.") .multilineTextAlignment(.center) .opacity(keyHasBeenReset ? 0.2 : 1.0) - Color(.secondarySystemBackground) - .frame(height: 1) - .frame(maxWidth: .infinity) - Button(action: { + SettingsButton("Reset Yubikey") { presentConfirmAlert.toggle() - }) { - VStack { - Text("Reset YubiKey") - .bold() - } - .padding(10) - .frame(maxWidth: .infinity) } .disabled(keyHasBeenReset) }