Skip to content

Commit

Permalink
Fixes a NFC issue after a FIDO reset over Lightning and various UI tw…
Browse files Browse the repository at this point in the history
…eaks in the configuration sub views.
  • Loading branch information
jensutbult committed Nov 5, 2024
1 parent d859d5a commit 530b733
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 28 deletions.
89 changes: 89 additions & 0 deletions Authenticator/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@
}
},
"Confirm OATH reset" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -1048,6 +1049,7 @@
}
},
"FIDO factory reset" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -1113,6 +1115,7 @@
}
},
"FIDO PIN protection" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand All @@ -1129,6 +1132,7 @@
}
},
"FIDO reset" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -1178,6 +1182,7 @@
}
},
"For additional security and to prevent unauthorized access the FIDO application can be protected by a PIN." : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand All @@ -1194,6 +1199,7 @@
}
},
"For additional security and to prevent unauthorized access the YubiKey can be password protected." : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -1708,7 +1714,24 @@
}
}
},
"OATH password" : {
"localizations" : {
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mots de passe OATH"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "OATHパスワード"
}
}
}
},
"OATH password protection" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -2453,6 +2476,22 @@
}
}
},
"Reset FIDO" : {
"localizations" : {
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Réinitialiser FIDO"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "FIDOのリセット"
}
}
}
},
"Reset FIDO application" : {
"comment" : "FIDO reset NFC confirmation message",
"localizations" : {
Expand Down Expand Up @@ -2587,6 +2626,7 @@
}
},
"Saved passwords has been cleared" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -3121,6 +3161,22 @@
}
}
},
"This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey." : {
"localizations" : {
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cela supprimera définitivement tous les comptes OATH TOTP/HOTP de votre YubiKey."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "これにより、すべてのOATH TOTP/HOTPアカウントがYubiKeyから削除されます。削除は取り消すことができません。"
}
}
}
},
"This will irrevocably delete all U2F and FIDO2 accounts, including passkeys, from your YubiKey." : {
"localizations" : {
"fr" : {
Expand Down Expand Up @@ -3169,6 +3225,38 @@
}
}
},
"To prevent unauthorized access the FIDO application can be protected by a PIN." : {
"localizations" : {
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pour éviter tout accès non autorisé, l'application FIDO peut être protégée par un code PIN."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "不正アクセスを防ぐため、FIDOアプリケーションはPINで保護することができる。"
}
}
}
},
"To prevent unauthorized access the OATH application can be protected by a password." : {
"localizations" : {
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pour empêcher tout accès non autorisé, l'application OATH peut être protégée par un mot de passe."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "不正アクセスを防ぐため、OATHアプリケーションはパスワードで保護することができます。"
}
}
}
},
"To prevent unauthorized access this YubiKey is protected with a password." : {
"comment" : "OATH password entry enter password",
"localizations" : {
Expand Down Expand Up @@ -3604,6 +3692,7 @@
}
},
"YubiKey has been reset" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down
17 changes: 15 additions & 2 deletions Authenticator/Model/ConfigurationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ class ConfigurationViewModel: ObservableObject {

let connection = Connection()

@Published var deviceInfo: YKFManagementDeviceInfo?
@Published var deviceInfo: YKFManagementDeviceInfo? {
didSet {
resetDeviceInfoTask?.cancel()
resetDeviceInfoTask = nil
}
}

var resetDeviceInfoTask: Task<Void, Error>?

init() {
Logger.allocation.debug("ConfigurationViewModel: init")
Expand Down Expand Up @@ -52,7 +59,13 @@ class ConfigurationViewModel: ObservableObject {
}

func start() {
deviceInfo = nil
resetDeviceInfoTask = Task.detached {
try? await Task.sleep(for: .seconds(1))
guard !Task.isCancelled else { return }
Task.detached { @MainActor in
self.deviceInfo = nil
}
}
waitForConnection()
}

Expand Down
13 changes: 8 additions & 5 deletions Authenticator/Model/Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,14 @@ class Connection: NSObject {
smartCardConnection?.stop()
accessoryConnection?.stop()
nfcConnection?.stop()
if YubiKitDeviceCapabilities.supportsMFIAccessoryKey {
YubiKitManager.shared.startAccessoryConnection()
}
if YubiKitDeviceCapabilities.supportsSmartCardOverUSBC {
YubiKitManager.shared.startSmartCardConnection()
// stop() returns immediately but closing the connection will take a few cycles so we need to wait to make sure it's closed before restarting.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if YubiKitDeviceCapabilities.supportsMFIAccessoryKey {
YubiKitManager.shared.startAccessoryConnection()
}
if YubiKitDeviceCapabilities.supportsSmartCardOverUSBC {
YubiKitManager.shared.startSmartCardConnection()
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions Authenticator/UI/YubiKeyConfiguration/ConfigurationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,11 @@ struct ConfigurationView: View {
Text("Manage PIN")
}
NavigationLink {
FIDOResetView()
.onDisappear {
FIDOResetView {
Task.detached { @MainActor in
model.start()
}
}
} label: {
ListIconView(image: Image(systemName: "trash"), color: Color(.systemRed), padding: 5)
Text("Reset FIDO application")
Expand Down
4 changes: 2 additions & 2 deletions Authenticator/UI/YubiKeyConfiguration/FIDOPINView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ struct FIDOPINView: View {

var body: some View {
SettingsView(image: Image(systemName: "lock.shield"), imageColor: Color(.systemPurple)) {
Text("FIDO PIN protection").font(.title2).bold()
Text("FIDO PIN").font(.title2).bold()

Text("For additional security and to prevent unauthorized access the FIDO application can be protected by a PIN.")
Text("To prevent unauthorized access the FIDO application can be protected by a PIN.")
.font(.subheadline)
.multilineTextAlignment(.center)

Expand Down
21 changes: 13 additions & 8 deletions Authenticator/UI/YubiKeyConfiguration/FIDOResetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fileprivate let resetMessageText = String(localized: "Your credentials, as well

struct FIDOResetView: View {

var completion: (() -> Void)
@Environment(\.dismiss) private var dismiss
@StateObject var model = FIDOResetViewModel()

Expand All @@ -29,20 +30,22 @@ struct FIDOResetView: View {
@State var messageText = resetMessageText
@State var enableResetButton = true
@State var errorMessage: String? = nil
@State var opacity = 1.0
@State var keyHasBeenReset = false
@State var image = Image(systemName: "exclamationmark.triangle")
@State var imageColor = Color(.systemRed)

var body: some View {
SettingsView(image: Image(systemName: "exclamationmark.triangle"), imageColor: Color(.systemRed)) {
Text("FIDO factory reset").font(.title2).bold().opacity(opacity)
ProgressView(value: progress, total: 4.0).opacity(opacity)
Text(messageText).font(.subheadline).multilineTextAlignment(.center)
SettingsView(image: image, imageColor: imageColor) {
Text("Reset FIDO application").font(.title2).bold().opacity(keyHasBeenReset ? 0.2 : 1.0)
ProgressView(value: progress, total: 4.0).opacity(keyHasBeenReset ? 0.2 : 1.0)
Text(messageText).font(.subheadline).multilineTextAlignment(.center).opacity(keyHasBeenReset ? 0.2 : 1.0)
} buttons: {
SettingsButton("Reset FIDO") {
presentConfirmAlert.toggle()
}
.disabled(!enableResetButton)
}
.navigationBarTitle(Text("FIDO reset"), displayMode: .inline)
.navigationBarTitle(Text("Reset FIDO"), displayMode: .inline)
.alert("Warning!", isPresented: $presentConfirmAlert, presenting: model, actions: { model in
Button(role: .destructive) {
presentConfirmAlert.toggle()
Expand Down Expand Up @@ -75,8 +78,8 @@ struct FIDOResetView: View {
updateState()
}
.onDisappear {
print("onDisappear")
model.cancelReset()
completion()
}
}

Expand All @@ -100,9 +103,11 @@ struct FIDOResetView: View {
self.messageText = String(localized: "Touch the button on the YubiKey now.", comment: "FIDO reset view")
case .success:
self.progress = 4.0
self.opacity = 0.5
self.keyHasBeenReset = true
self.enableResetButton = false
self.messageText = String(localized: "The FIDO application of your YubiKey has been reset to factory defaults.", comment: "FIDO reset view")
self.image = Image(systemName: "checkmark.circle")
self.imageColor = Color(.systemGreen)
case .error(let error):
self.enableResetButton = true
self.presentErrorAlert = true
Expand Down
4 changes: 2 additions & 2 deletions Authenticator/UI/YubiKeyConfiguration/OATHPasswordView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ struct OATHPasswordView: View {

var body: some View {
SettingsView(image: Image(systemName: "lock.shield"), imageColor: Color(.systemPurple)) {
Text("OATH password protection").font(.title2).bold()
Text("For additional security and to prevent unauthorized access the YubiKey can be password protected.")
Text("OATH password").font(.title2).bold()
Text("To prevent unauthorized access the OATH application can be protected by a password.")
.font(.subheadline)
.multilineTextAlignment(.center)
} buttons: {
Expand Down
17 changes: 12 additions & 5 deletions Authenticator/UI/YubiKeyConfiguration/OATHResetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,25 @@ struct OATHResetView: View {
@State var presentErrorAlert = false
@State var keyHasBeenReset = false
@State var errorMessage: String? = nil

@State var image = Image(systemName: "exclamationmark.triangle")
@State var imageColor = Color(.systemRed)

var body: some View {
SettingsView(image: Image(systemName: "exclamationmark.triangle"), imageColor: Color(.systemRed)) {
Text(keyHasBeenReset ? String(localized: "YubiKey has been reset") : String(localized: "Reset OATH application")).font(.title2).bold()
SettingsView(image: image, imageColor: imageColor) {
Text(String(localized: "Reset OATH application")).font(.title2).bold()
.opacity(keyHasBeenReset ? 0.2 : 1.0)
Text("Reset all accounts stored on YubiKey, make sure they are not in use anywhere before doing this.")
.font(.subheadline)
.multilineTextAlignment(.center)
.opacity(keyHasBeenReset ? 0.2 : 1.0)
} buttons: {
SettingsButton("Reset YubiKey") {
SettingsButton("Reset OATH") {
presentConfirmAlert.toggle()
}
.disabled(keyHasBeenReset)
}
.navigationBarTitle(Text("Reset OATH"), displayMode: .inline)
.alert("Confirm OATH reset", isPresented: $presentConfirmAlert, presenting: model, actions: { model in
.alert("Warning!", isPresented: $presentConfirmAlert, presenting: model, actions: { model in
Button(role: .destructive) {
presentConfirmAlert.toggle()
model.reset()
Expand All @@ -50,6 +53,8 @@ struct OATHResetView: View {
} label: {
Text("Cancel")
}
}, message: { _ in
Text("This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.")
})
.alert(errorMessage ?? String(localized: "Unknown error"), isPresented: $presentErrorAlert, actions: { })
.onChange(of: model.state) { state in
Expand All @@ -59,6 +64,8 @@ struct OATHResetView: View {
self.keyHasBeenReset = false
case .success:
self.keyHasBeenReset = true
self.image = Image(systemName: "checkmark.circle")
self.imageColor = Color(.systemGreen)
case .error(let message):
self.presentErrorAlert = true
self.errorMessage = message
Expand Down
12 changes: 10 additions & 2 deletions Authenticator/UI/YubiKeyConfiguration/OATHSavedPasswordsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ struct OATHSavedPasswordsView: View {
@State var presentErrorAlert = false
@State var errorMessage: String? = nil
@State var passwordsHasBeenCleared = false
@State var image = Image(systemName: "xmark.circle")
@State var imageColor = Color(.systemRed)

var body: some View {
SettingsView(image: Image(systemName: "xmark.circle"), imageColor: Color(.systemRed)) {
Text(passwordsHasBeenCleared ? "Saved passwords has been cleared" : "Clear saved OATH passwords").font(.title2).bold()
SettingsView(image: image, imageColor: imageColor) {
Text("Clear saved OATH passwords")
.multilineTextAlignment(.center)
.font(.title2)
.bold()
.opacity(passwordsHasBeenCleared ? 0.2 : 1.0)
Text("Clear passwords saved on this device. This will prompt for a password next time a password protected YubiKey is used.")
.font(.subheadline)
.multilineTextAlignment(.center)
Expand Down Expand Up @@ -58,6 +64,8 @@ struct OATHSavedPasswordsView: View {
self.passwordsHasBeenCleared = false
case .success:
self.passwordsHasBeenCleared = true
self.image = Image(systemName: "checkmark.circle")
self.imageColor = Color(.systemGreen)
case .error(let message):
self.presentErrorAlert = true
self.errorMessage = message
Expand Down

0 comments on commit 530b733

Please sign in to comment.