From 2d89990933dab2528dc754a20456fc96a93e261e Mon Sep 17 00:00:00 2001 From: zapomnij Date: Sun, 17 Mar 2024 13:19:30 +0100 Subject: [PATCH] App is now more asynchronical OATH: added base32 error handler, app will not crash LibrePassCipherView: added alert which indicates a bad 2FA configuration, added copy button for copying TOTP one time password LibrePassManagerWindow: some tasks have been made asynchronical --- LibrePass/API/OATH.swift | 9 +- LibrePass/LibrePassCipherView.swift | 148 ++++++++++++------------- LibrePass/LibrePassManagerWindow.swift | 30 ++--- 3 files changed, 96 insertions(+), 91 deletions(-) diff --git a/LibrePass/API/OATH.swift b/LibrePass/API/OATH.swift index c1ff9a0..f951908 100644 --- a/LibrePass/API/OATH.swift +++ b/LibrePass/API/OATH.swift @@ -63,7 +63,7 @@ struct OATHParams { var period: Int = 30 var counter: Int = 0 - init(uri: String) { + init(uri: String) throws { self.uri = uri if uri.starts(with: "otpauth://totp/") { @@ -74,13 +74,16 @@ struct OATHParams { let uriSplit = uri.components(separatedBy: "?")[1].components(separatedBy: "&") - uriSplit.forEach { keyVal in + for keyVal in uriSplit { let split = keyVal.components(separatedBy: "=") let key = split[0], val = split[1] switch key { case "secret": - self.secret = Data(base32Decode(val)!) + guard let secret = base32Decode(val) else { + throw LibrePassApiErrors.WithMessage(message: "Bad 2FA secret") + } + self.secret = Data(secret) break case "algorithm": self.algorithm = val.toOTPAlgorithm() diff --git a/LibrePass/LibrePassCipherView.swift b/LibrePass/LibrePassCipherView.swift index f5b09a8..40ef717 100644 --- a/LibrePass/LibrePassCipherView.swift +++ b/LibrePass/LibrePassCipherView.swift @@ -82,6 +82,12 @@ struct CipherLoginDataView: View { Section(header: Text("Two factor")) { if let _ = self.twoFactorUri { HStack { + Button(action: { + UIPasteboard.general.string = self.oneTimePassword + }, label: { + Image(systemName: "doc.on.doc") + }) + .buttonStyle(.plain) Text(self.oneTimePassword) Spacer() Text(String(self.timeLeft)) @@ -102,6 +108,12 @@ struct CipherLoginDataView: View { stop = running self.twoFactorUri = nil } + + .alert("Incorrect 2FA configuration", isPresented: self.$twoFactorError) { + Button("OK", role: .cancel) { + self.twoFactorUri = nil + } + } } else { Button("Set up 2FA") { self.editTwoFactor = true @@ -142,44 +154,59 @@ struct CipherLoginDataView: View { runAuthenticatorJob() }) { List { - TextField("Secret", text: self.$twoFactorSecret) - Picker("Type", selection: self.$twoFactorType) { - Text("TOTP").tag(OATHParams.OATHType.TOTP) - Text("HOTP").tag(OATHParams.OATHType.HOTP) - } - TextField("Digits", value: self.$twoFactorDigits, formatter: NumberFormatter()) - if self.twoFactorType == .TOTP { - TextField("Period", value: self.$twoFactorPeriod, formatter: NumberFormatter()) - } else { - TextField("Counter", value: self.$twoFactorCounter, formatter: NumberFormatter()) - } - - Button("Apply") { - var str = "otpauth://" + self.twoFactorType.toString() - str += "/randomlabel?secret=" + self.twoFactorSecret - str += "&algorithm=" + self.twoFactorAlgorithm.toString() - str += "&digits=" + String(self.twoFactorDigits) - + Section(header: Text("Manual configuration")) { + TextField("Secret", text: self.$twoFactorSecret) + Picker("Type", selection: self.$twoFactorType) { + Text("TOTP").tag(OATHParams.OATHType.TOTP) + } + TextField("Digits", value: self.$twoFactorDigits, formatter: NumberFormatter()) if self.twoFactorType == .TOTP { - str += "&period=" + String(self.twoFactorPeriod) + TextField("Period", value: self.$twoFactorPeriod, formatter: NumberFormatter()) } else { - str += "&counter=" + String(self.twoFactorCounter) + TextField("Counter", value: self.$twoFactorCounter, formatter: NumberFormatter()) } - self.twoFactorUri = str - - self.editTwoFactor = false + Button("Apply") { + let split = self.twoFactorSecret.components(separatedBy: " ") + if split.count > 0 { + self.twoFactorSecret = "" + split.forEach { + if $0 != "" { + self.twoFactorSecret += $0 + } + } + } + + var str = "otpauth://" + self.twoFactorType.toString() + str += "/randomlabel?secret=" + self.twoFactorSecret + str += "&algorithm=" + self.twoFactorAlgorithm.toString() + str += "&digits=" + String(self.twoFactorDigits) + + if self.twoFactorType == .TOTP { + str += "&period=" + String(self.twoFactorPeriod) + } else { + str += "&counter=" + String(self.twoFactorCounter) + } + + self.twoFactorUri = str + + self.editTwoFactor = false + } } } .onAppear { - if let twoFactorUri = self.twoFactorUri { - let params = OATHParams(uri: twoFactorUri) - self.twoFactorType = params.type - self.twoFactorAlgorithm = params.algorithm - self.twoFactorSecret = base32Encode(params.secret) - self.twoFactorDigits = params.digits - self.twoFactorPeriod = params.period - self.twoFactorCounter = params.counter + do { + if let twoFactorUri = self.twoFactorUri { + let params = try OATHParams(uri: twoFactorUri) + self.twoFactorType = params.type + self.twoFactorAlgorithm = params.algorithm + self.twoFactorSecret = base32Encode(params.secret) + self.twoFactorDigits = params.digits + self.twoFactorPeriod = params.period + self.twoFactorCounter = params.counter + } + } catch { + self.twoFactorError = true } } } @@ -191,21 +218,26 @@ struct CipherLoginDataView: View { @State var twoFactorDigits = 6 @State var twoFactorPeriod = 30 @State var twoFactorCounter = 0 + @State var twoFactorError = false func runAuthenticatorJob() { Task { - if let twoFactorUri = self.twoFactorUri { - let engine = OATHParams(uri: twoFactorUri) - switch engine.type { - case .TOTP: - await engine.runTOTPCounter { oneTimePassword, timeLeft in - self.oneTimePassword = oneTimePassword - self.timeLeft = timeLeft + do { + if let twoFactorUri = self.twoFactorUri { + let engine = try OATHParams(uri: twoFactorUri) + switch engine.type { + case .TOTP: + await engine.runTOTPCounter { oneTimePassword, timeLeft in + self.oneTimePassword = oneTimePassword + self.timeLeft = timeLeft + } + break + case .HOTP: + break } - break - case .HOTP: - break } + } catch { + self.twoFactorError = true } } } @@ -354,37 +386,3 @@ struct CipherButton: View { } } } - -struct CipherDeleteButton: View { - @Environment(\.presentationMode) var presentationMode - - @Binding var lClient: LibrePassClient - var id: String - - @State var areYouSure = false - @State var errorString = String() - @State var showAlert = false - - var body: some View { - Button(action: { - self.areYouSure = true - }) { - Image(systemName: "trash") - } - .foregroundColor(Color.red) - - .alert("Are you sure you want to delete this cipher?", isPresented: $areYouSure) { - Button("Yes", role: .destructive) { - _ = try? lClient.delete(id: id) - - self.presentationMode.wrappedValue.dismiss() - } - - Button("No", role: .cancel) {} - } - - .alert(self.errorString, isPresented: $showAlert) { - Button("OK", role: .cancel) {} - } - } -} diff --git a/LibrePass/LibrePassManagerWindow.swift b/LibrePass/LibrePassManagerWindow.swift index 990b617..aa87adb 100644 --- a/LibrePass/LibrePassManagerWindow.swift +++ b/LibrePass/LibrePassManagerWindow.swift @@ -122,23 +122,27 @@ struct LibrePassManagerWindow: View { } func deleteCiphers() throws { - for index in self.toDelete { - try self.lClient.delete(id: self.lClient.vault.vault[index].id) + Task { + for index in self.toDelete { + try self.lClient.delete(id: self.lClient.vault.vault[index].id) + } } } func newCipher(type: LibrePassCipher.CipherType) { - let cipher = LibrePassCipher(id: lClient.generateId(), owner: lClient.credentialsDatabase!.userId, type: type) - - do { - try self.lClient.put(cipher: cipher) - try self.syncVault() - } catch ApiClientErrors.StatusCodeNot200(let statusCode, let body){ - self.errorString = String(statusCode) + ": " + body.error - self.showAlert = true - } catch { - self.errorString = error.localizedDescription - self.showAlert = true + Task { + let cipher = LibrePassCipher(id: lClient.generateId(), owner: lClient.credentialsDatabase!.userId, type: type) + + do { + try self.lClient.put(cipher: cipher) + try self.syncVault() + } catch ApiClientErrors.StatusCodeNot200(let statusCode, let body){ + self.errorString = String(statusCode) + ": " + body.error + self.showAlert = true + } catch { + self.errorString = error.localizedDescription + self.showAlert = true + } } } }