From b643e081a58ae9991862115e68ec5523e8a36f72 Mon Sep 17 00:00:00 2001 From: Joel-David Date: Sat, 28 Sep 2024 12:20:28 +0800 Subject: [PATCH 1/6] bump packages --- Chronos.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Chronos.xcodeproj/project.pbxproj b/Chronos.xcodeproj/project.pbxproj index b02c4df..6758704 100644 --- a/Chronos.xcodeproj/project.pbxproj +++ b/Chronos.xcodeproj/project.pbxproj @@ -1299,7 +1299,7 @@ repositoryURL = "https://github.com/ggruen/CloudKitSyncMonitor"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.1; + minimumVersion = 2.0.0; }; }; 6B35A4C72B557EE50004D4C5 /* XCRemoteSwiftPackageReference "AlertKit" */ = { @@ -1339,7 +1339,7 @@ repositoryURL = "https://github.com/twostraws/CodeScanner.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.4.1; + minimumVersion = 2.5.1; }; }; 6B7383DE2B9C3962008E8867 /* XCRemoteSwiftPackageReference "Factory" */ = { From 93e8dbb3d706274ac06370b27b1ee902338dc638 Mon Sep 17 00:00:00 2001 From: Joel-David Date: Sun, 6 Oct 2024 23:54:03 +0800 Subject: [PATCH 2/6] Updated show error --- .../Tokens/AddToken/AddManualTokenView.swift | 2 +- Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift | 72 +++++++-------- .../App/Tabs/Tokens/Row/TokenRowView.swift | 90 +++++++++++-------- .../App/Tabs/Tokens/Row/UpdateTokenView.swift | 2 +- Chronos/Data/Token.swift | 14 +-- 5 files changed, 96 insertions(+), 84 deletions(-) diff --git a/Chronos/App/Tabs/Tokens/AddToken/AddManualTokenView.swift b/Chronos/App/Tabs/Tokens/AddToken/AddManualTokenView.swift index f269966..8fbac6f 100644 --- a/Chronos/App/Tabs/Tokens/AddToken/AddManualTokenView.swift +++ b/Chronos/App/Tabs/Tokens/AddToken/AddManualTokenView.swift @@ -152,6 +152,6 @@ extension AddManualTokenView { tempToken.counter = counter tempToken.period = period - return tempToken.validate() + return tempToken.isValid } } diff --git a/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift b/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift index 036048a..f7a9eac 100644 --- a/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift +++ b/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift @@ -13,46 +13,38 @@ struct TOTPRowView: View { var body: some View { Group { - if token.validate() { - Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateTOTP()) - .font(.largeTitle) - .fontWeight(.light) - .lineLimit(1) - .onAppear(perform: updateOtp) - - Spacer() - - ZStack(alignment: .leading) { - Circle() - .stroke( - Color.gray.opacity(0.5), - lineWidth: 2 - ) - .frame(width: 28, height: 28) - - Circle() - .trim(from: 0.0, to: progress) - .stroke( - Color.white.opacity(0.8), - lineWidth: 2 - ) - .rotationEffect(.degrees(-90)) - .frame(width: 28, height: 28) - - Text(String(secsLeft)) - .font(.system(size: 12)) - .frame(width: 28, height: 28, alignment: .center) - } - .onAppear(perform: updateProgress) - .onReceive(timer) { _ in - updateProgress() - } - } else { - Text("Invalid Token") - .font(.title) - .fontWeight(.light) - .opacity(0.5) - .lineLimit(1) + Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateTOTP()) + .font(.largeTitle) + .fontWeight(.light) + .lineLimit(1) + .onAppear(perform: updateOtp) + + Spacer() + + ZStack(alignment: .leading) { + Circle() + .stroke( + Color.gray.opacity(0.5), + lineWidth: 2 + ) + .frame(width: 28, height: 28) + + Circle() + .trim(from: 0.0, to: progress) + .stroke( + Color.white.opacity(0.8), + lineWidth: 2 + ) + .rotationEffect(.degrees(-90)) + .frame(width: 28, height: 28) + + Text(String(secsLeft)) + .font(.system(size: 12)) + .frame(width: 28, height: 28, alignment: .center) + } + .onAppear(perform: updateProgress) + .onReceive(timer) { _ in + updateProgress() } } } diff --git a/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift b/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift index b19f0d6..1b6cf0e 100644 --- a/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift +++ b/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift @@ -65,11 +65,19 @@ struct TokenRowView: View { } } else { HStack { - switch token.type { - case TokenTypeEnum.TOTP: - TOTPRowView(token: token, timer: timer) - case TokenTypeEnum.HOTP: - HOTPRowView(token: token, encryptedToken: encryptedToken) + if token.isValid { + switch token.type { + case TokenTypeEnum.TOTP: + TOTPRowView(token: token, timer: timer) + case TokenTypeEnum.HOTP: + HOTPRowView(token: token, encryptedToken: encryptedToken) + } + } else { + Text("Invalid Token") + .font(.title) + .fontWeight(.light) + .opacity(0.5) + .lineLimit(1) } } } @@ -79,19 +87,29 @@ struct TokenRowView: View { .listRowBackground(Color(red: 0.04, green: 0, blue: 0.11)) .onTapGesture { if !stateTapToRevealEnabled { - switch token.type { - case TokenTypeEnum.TOTP: - UIPasteboard.general.string = token.generateTOTP() - case TokenTypeEnum.HOTP: - UIPasteboard.general.string = token.generateHOTP() - } + if token.isValid { + switch token.type { + case TokenTypeEnum.TOTP: + UIPasteboard.general.string = token.generateTOTP() + case TokenTypeEnum.HOTP: + UIPasteboard.general.string = token.generateHOTP() + } - AlertKitAPI.present( - title: "Copied", - icon: .done, - style: .iOS17AppleMusic, - haptic: .success - ) + AlertKitAPI.present( + title: "Copied", + icon: .done, + style: .iOS17AppleMusic, + haptic: .success + ) + } else { + AlertKitAPI.present( + title: "Invalid Token", + subtitle: token.validationError?.localizedDescription.description, + icon: .error, + style: .iOS17AppleMusic, + haptic: .success + ) + } } else { tokenRevealed.toggle() } @@ -159,27 +177,29 @@ struct TokenRowView: View { } .tint(.blue) - Button { - token.pinned = !(token.pinned ?? false) - cryptoService.updateEncryptedToken(encryptedToken: encryptedToken, token: token) - triggerSortAndFilterTokenPairs() - } label: { - VStack(alignment: .center) { - Image(systemName: token.pinned ?? false ? "pin.slash" : "pin") - Text(token.pinned ?? false ? "Unpin" : "Pin") + if token.isValid { + Button { + token.pinned = !(token.pinned ?? false) + cryptoService.updateEncryptedToken(encryptedToken: encryptedToken, token: token) + triggerSortAndFilterTokenPairs() + } label: { + VStack(alignment: .center) { + Image(systemName: token.pinned ?? false ? "pin.slash" : "pin") + Text(token.pinned ?? false ? "Unpin" : "Pin") + } } - } - .tint(.indigo) - - Button { - self.showTokenQRSheet = true - } label: { - VStack(alignment: .center) { - Image(systemName: "qrcode") - Text("QR") + .tint(.indigo) + + Button { + self.showTokenQRSheet = true + } label: { + VStack(alignment: .center) { + Image(systemName: "qrcode") + Text("QR") + } } + .tint(.gray) } - .tint(.gray) } } diff --git a/Chronos/App/Tabs/Tokens/Row/UpdateTokenView.swift b/Chronos/App/Tabs/Tokens/Row/UpdateTokenView.swift index c2bd2c7..931d827 100644 --- a/Chronos/App/Tabs/Tokens/Row/UpdateTokenView.swift +++ b/Chronos/App/Tabs/Tokens/Row/UpdateTokenView.swift @@ -142,7 +142,7 @@ extension UpdateTokenView { tempToken.counter = counter tempToken.period = period - return tempToken.validate() + return tempToken.isValid } var hasChanged: Bool { diff --git a/Chronos/Data/Token.swift b/Chronos/Data/Token.swift index 0bc7a39..7de7629 100644 --- a/Chronos/Data/Token.swift +++ b/Chronos/Data/Token.swift @@ -34,16 +34,16 @@ class Token: Codable, Identifiable { // Extra Data var pinned: Bool? = false - func validate() -> Bool { + var isValid: Bool { + return validationError == nil + } + + var validationError: Error? { do { try TokenValidator.validate(token: self) - return true - } catch let error as OTPError { - print("Validation failed: \(error.localizedDescription)") - return false + return nil } catch { - print("An unexpected error occurred: \(error)") - return false + return error } } From 2a96052342e6f9c33583e3177202556b3f802e00 Mon Sep 17 00:00:00 2001 From: Joel-David Date: Mon, 7 Oct 2024 00:01:24 +0800 Subject: [PATCH 3/6] optimised code --- Chronos/App/Tabs/Tokens/Row/HOTPRowView.swift | 8 ++++---- Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift | 4 ++-- Chronos/App/Tabs/Tokens/Row/TokenRowView.swift | 7 +------ Chronos/Data/Token.swift | 13 +++++++++++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Chronos/App/Tabs/Tokens/Row/HOTPRowView.swift b/Chronos/App/Tabs/Tokens/Row/HOTPRowView.swift index 5012262..b16dc42 100644 --- a/Chronos/App/Tabs/Tokens/Row/HOTPRowView.swift +++ b/Chronos/App/Tabs/Tokens/Row/HOTPRowView.swift @@ -13,15 +13,15 @@ struct HOTPRowView: View { var encryptedToken: EncryptedToken var body: some View { - Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateHOTP()) + Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateOtp()) .font(.largeTitle) .fontWeight(.light) .lineLimit(1) .onAppear { - otp = token.generateHOTP() + otp = token.generateOtp() } .onChange(of: token.counter) { _, _ in - otp = token.generateHOTP() + otp = token.generateOtp() } Spacer() Button { @@ -30,7 +30,7 @@ struct HOTPRowView: View { token.counter += 1 cryptoService.updateEncryptedToken(encryptedToken: encryptedToken, token: token) - otp = token.generateHOTP() + otp = token.generateOtp() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { disableIncrementBtn = false } diff --git a/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift b/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift index f7a9eac..85cf818 100644 --- a/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift +++ b/Chronos/App/Tabs/Tokens/Row/TOTPRowView.swift @@ -13,7 +13,7 @@ struct TOTPRowView: View { var body: some View { Group { - Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateTOTP()) + Text(!otp.isEmpty ? formatOtp(otp: otp) : token.generateOtp()) .font(.largeTitle) .fontWeight(.light) .lineLimit(1) @@ -50,7 +50,7 @@ struct TOTPRowView: View { } private func updateOtp() { - otp = token.generateTOTP() + otp = token.generateOtp() } private func updateProgress() { diff --git a/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift b/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift index 1b6cf0e..3205b4d 100644 --- a/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift +++ b/Chronos/App/Tabs/Tokens/Row/TokenRowView.swift @@ -88,12 +88,7 @@ struct TokenRowView: View { .onTapGesture { if !stateTapToRevealEnabled { if token.isValid { - switch token.type { - case TokenTypeEnum.TOTP: - UIPasteboard.general.string = token.generateTOTP() - case TokenTypeEnum.HOTP: - UIPasteboard.general.string = token.generateHOTP() - } + UIPasteboard.general.string = token.generateOtp() AlertKitAPI.present( title: "Copied", diff --git a/Chronos/Data/Token.swift b/Chronos/Data/Token.swift index 7de7629..a9cd844 100644 --- a/Chronos/Data/Token.swift +++ b/Chronos/Data/Token.swift @@ -47,7 +47,16 @@ class Token: Codable, Identifiable { } } - func generateTOTP() -> String { + func generateOtp() -> String { + switch type { + case .TOTP: + return generateTOTP() + case .HOTP: + return generateHOTP() + } + } + + private func generateTOTP() -> String { let digits = digits let secret = base32DecodeToData(secret)! let period = period @@ -67,7 +76,7 @@ class Token: Codable, Identifiable { return totp!.generate(time: Date()) ?? "" } - func generateHOTP() -> String { + private func generateHOTP() -> String { let digits = digits let secret = base32DecodeToData(secret)! let counter = counter From fce534b076f8c22b618f5cf256616d43421e5200 Mon Sep 17 00:00:00 2001 From: Joel-David Date: Mon, 7 Oct 2024 00:12:44 +0800 Subject: [PATCH 4/6] Fixed OtpAuthUrlParser and tests --- Chronos/Helper/OtpAuthUrlParser.swift | 25 ++++++++--- ChronosTests/Token/ParseOtpAuthUrl.swift | 44 +++++++++---------- .../Token/QrCodeGenerationAndParsing.swift | 36 +++++++-------- ChronosTests/Token/TokenToOtpAuthUrl.swift | 36 +++++++-------- 4 files changed, 71 insertions(+), 70 deletions(-) diff --git a/Chronos/Helper/OtpAuthUrlParser.swift b/Chronos/Helper/OtpAuthUrlParser.swift index 360c1ca..9792041 100644 --- a/Chronos/Helper/OtpAuthUrlParser.swift +++ b/Chronos/Helper/OtpAuthUrlParser.swift @@ -18,12 +18,25 @@ public class OtpAuthUrlParser { var path = components.path if !path.isEmpty { path.remove(at: path.startIndex) - let labels = path.split(separator: ":", maxSplits: 1).map { String($0) } - if labels.count == 2 { - token.issuer = labels[0] - token.account = labels[1] - } else if labels.count == 1 { - path.hasPrefix(":") ? (token.account = labels[0]) : (token.issuer = labels[0]) + + if !path.contains(":") { + token.account = path + } else { + let label = path.split(separator: ":", maxSplits: 1).map { String($0) } + + if label.count == 2 { + token.issuer = label[0] + token.account = label[1] + } + + // Label is malformed e.g. :Account or Issuer: + if label.count == 1 { + if path.hasPrefix(":") { + token.account = label[0] + } else if path.hasSuffix(":") { + token.issuer = label[0] + } + } } } diff --git a/ChronosTests/Token/ParseOtpAuthUrl.swift b/ChronosTests/Token/ParseOtpAuthUrl.swift index f0c2509..acb4fb2 100644 --- a/ChronosTests/Token/ParseOtpAuthUrl.swift +++ b/ChronosTests/Token/ParseOtpAuthUrl.swift @@ -3,11 +3,9 @@ import XCTest final class ParseOtpAuthUrlTests: XCTestCase { func testTotp() throws { - let otpService = OTPService() - // Test case 1: Standard TOTP token do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") XCTAssertEqual(token.issuer, "Apple") XCTAssertEqual(token.account, "john@appleseed.com") XCTAssertEqual(token.period, 30) @@ -21,7 +19,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 2: TOTP token without account do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") XCTAssertEqual(token.issuer, "Apple") XCTAssertEqual(token.account, "") XCTAssertEqual(token.period, 30) @@ -35,7 +33,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 3: TOTP token with different period and algorithm do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=Apple&period=45") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=Apple&period=45") XCTAssertEqual(token.issuer, "Apple") XCTAssertEqual(token.account, "john@appleseed.com") XCTAssertEqual(token.period, 45) @@ -49,7 +47,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 4: TOTP token without issuer do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/john@doe.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/john@doe.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") XCTAssertEqual(token.issuer, "") XCTAssertEqual(token.account, "john@doe.com") XCTAssertEqual(token.period, 30) @@ -63,7 +61,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 5: TOTP token with special characters in account and issuer do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/My%20Company:john.doe+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/My%20Company:john.doe+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&period=30") XCTAssertEqual(token.issuer, "My Company") XCTAssertEqual(token.account, "john.doe+test@mycompany.com") XCTAssertEqual(token.period, 30) @@ -77,7 +75,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 6: TOTP token with no issuer and no account do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") XCTAssertEqual(token.issuer, "") XCTAssertEqual(token.account, "") XCTAssertEqual(token.period, 30) @@ -91,7 +89,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 7: TOTP token with different digits and SHA512 algorithm do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Google:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&period=60") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Google:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&period=60") XCTAssertEqual(token.issuer, "Google") XCTAssertEqual(token.account, "alice@google.com") XCTAssertEqual(token.period, 60) @@ -105,7 +103,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 8: TOTP token with empty secret do { - _ = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&period=30") + _ = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&period=30") XCTFail("Expected OTPError.invalidSecret but no error was thrown") } catch OTPError.invalidSecret { // Success: expected error was thrown @@ -115,7 +113,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 9: Standard TOTP token with lowercase algorithm do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha1&digits=6&issuer=GitHub&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha1&digits=6&issuer=GitHub&period=30") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.period, 30) @@ -129,7 +127,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 10: Standard TOTP token with lowercase algorithm non default do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=6&issuer=GitHub&period=30") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=6&issuer=GitHub&period=30") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.period, 30) @@ -143,11 +141,9 @@ final class ParseOtpAuthUrlTests: XCTestCase { } func testHotp() throws { - let otpService = OTPService() - // Test case 1: Standard HOTP token do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.counter, 1) @@ -161,7 +157,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 2: HOTP token without account do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "") XCTAssertEqual(token.counter, 1) @@ -175,7 +171,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 3: HOTP token with different counter and algorithm do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=GitHub&counter=10") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=GitHub&counter=10") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.counter, 10) @@ -189,7 +185,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 4: HOTP token without issuer do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") XCTAssertEqual(token.issuer, "") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.counter, 1) @@ -203,7 +199,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 5: HOTP token with special characters in account and issuer do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/My%20Company:user+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/My%20Company:user+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&counter=1") XCTAssertEqual(token.issuer, "My Company") XCTAssertEqual(token.account, "user+test@mycompany.com") XCTAssertEqual(token.counter, 1) @@ -217,7 +213,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 6: HOTP token with no issuer and no account do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") XCTAssertEqual(token.issuer, "") XCTAssertEqual(token.account, "") XCTAssertEqual(token.counter, 1) @@ -231,7 +227,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 7: HOTP token with different digits and SHA512 algorithm do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Google:user@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&counter=100") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Google:user@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&counter=100") XCTAssertEqual(token.issuer, "Google") XCTAssertEqual(token.account, "user@google.com") XCTAssertEqual(token.counter, 100) @@ -245,7 +241,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 8: HOTP token with empty secret do { - _ = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&counter=1") + _ = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&counter=1") XCTFail("Expected OTPError.invalidSecret but no error was thrown") } catch OTPError.invalidSecret { // Success: expected error was thrown @@ -255,7 +251,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 9: Standard HOTP token with lowercase algorithm do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha1&digits=6&issuer=GitHub&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha1&digits=6&issuer=GitHub&counter=1") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.counter, 1) @@ -269,7 +265,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { // Test case 10: Standard HOTP token with lowercase algorithm non default do { - let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=6&issuer=GitHub&counter=1") + let token = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=6&issuer=GitHub&counter=1") XCTAssertEqual(token.issuer, "GitHub") XCTAssertEqual(token.account, "user@github.com") XCTAssertEqual(token.counter, 1) diff --git a/ChronosTests/Token/QrCodeGenerationAndParsing.swift b/ChronosTests/Token/QrCodeGenerationAndParsing.swift index c9f43a7..b490817 100644 --- a/ChronosTests/Token/QrCodeGenerationAndParsing.swift +++ b/ChronosTests/Token/QrCodeGenerationAndParsing.swift @@ -5,8 +5,6 @@ import XCTest final class QrCodeGenerationAndParsingTests: XCTestCase { func testTotp() throws { - let otpService = OTPService() - // Test case 1: Standard TOTP token let token1 = Token() token1.issuer = "Apple" @@ -17,7 +15,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token1.type = TokenTypeEnum.TOTP token1.algorithm = TokenAlgorithmEnum.SHA1 - var url = otpService.tokenToOtpAuthUrl(token: token1)! + var url = token1.otpAuthUrl()! let qr1 = try QRCode.build .text(url) .generate @@ -36,7 +34,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token2.type = TokenTypeEnum.TOTP token2.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token2)! + url = token2.otpAuthUrl()! let qr2 = try QRCode.build .text(url) .generate @@ -56,7 +54,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token3.type = TokenTypeEnum.TOTP token3.algorithm = TokenAlgorithmEnum.SHA256 - url = otpService.tokenToOtpAuthUrl(token: token3)! + url = token3.otpAuthUrl()! let qr3 = try QRCode.build .text(url) .generate @@ -75,7 +73,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token4.type = TokenTypeEnum.TOTP token4.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token4)! + url = token4.otpAuthUrl()! let qr4 = try QRCode.build .text(url) .generate @@ -95,7 +93,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token5.type = TokenTypeEnum.TOTP token5.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token5)! + url = token5.otpAuthUrl()! let qr5 = try QRCode.build .text(url) .generate @@ -112,7 +110,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token6.type = TokenTypeEnum.TOTP token6.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token6)! + url = token6.otpAuthUrl()! let qr6 = try QRCode.build .text(url) .generate @@ -131,7 +129,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token7.type = TokenTypeEnum.TOTP token7.algorithm = TokenAlgorithmEnum.SHA512 - url = otpService.tokenToOtpAuthUrl(token: token7)! + url = token7.otpAuthUrl()! let qr7 = try QRCode.build .text(url) .generate @@ -150,7 +148,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token8.type = TokenTypeEnum.TOTP token8.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token8)! + url = token8.otpAuthUrl()! let qr8 = try QRCode.build .text(url) .generate @@ -161,8 +159,6 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { } func testHotp() throws { - let otpService = OTPService() - // Test case 1: Standard HOTP token let token1 = Token() token1.issuer = "GitHub" @@ -173,7 +169,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token1.type = TokenTypeEnum.HOTP token1.algorithm = TokenAlgorithmEnum.SHA1 - var url = otpService.tokenToOtpAuthUrl(token: token1)! + var url = token1.otpAuthUrl()! let qr1 = try QRCode.build .text(url) .generate @@ -191,7 +187,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token2.type = TokenTypeEnum.HOTP token2.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token2)! + url = token2.otpAuthUrl()! let qr2 = try QRCode.build .text(url) .generate @@ -210,7 +206,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token3.type = TokenTypeEnum.HOTP token3.algorithm = TokenAlgorithmEnum.SHA256 - url = otpService.tokenToOtpAuthUrl(token: token3)! + url = token3.otpAuthUrl()! let qr3 = try QRCode.build .text(url) .generate @@ -228,7 +224,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token4.type = TokenTypeEnum.HOTP token4.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token4)! + url = token4.otpAuthUrl()! let qr4 = try QRCode.build .text(url) .generate @@ -247,7 +243,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token5.type = TokenTypeEnum.HOTP token5.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token5)! + url = token5.otpAuthUrl()! let qr5 = try QRCode.build .text(url) .generate @@ -264,7 +260,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token6.type = TokenTypeEnum.HOTP token6.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token6)! + url = token6.otpAuthUrl()! let qr6 = try QRCode.build .text(url) .generate @@ -283,7 +279,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token7.type = TokenTypeEnum.HOTP token7.algorithm = TokenAlgorithmEnum.SHA512 - url = otpService.tokenToOtpAuthUrl(token: token7)! + url = token7.otpAuthUrl()! let qr7 = try QRCode.build .text(url) .generate @@ -302,7 +298,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { token8.type = TokenTypeEnum.HOTP token8.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token8)! + url = token8.otpAuthUrl()! let qr8 = try QRCode.build .text(url) .generate diff --git a/ChronosTests/Token/TokenToOtpAuthUrl.swift b/ChronosTests/Token/TokenToOtpAuthUrl.swift index bc1975d..8bac4fc 100644 --- a/ChronosTests/Token/TokenToOtpAuthUrl.swift +++ b/ChronosTests/Token/TokenToOtpAuthUrl.swift @@ -3,8 +3,6 @@ import XCTest final class TokenToOtpAuthUrlTests: XCTestCase { func testTotp() throws { - let otpService = OTPService() - // Test case 1: Standard TOTP token let token1 = Token() token1.issuer = "Apple" @@ -15,7 +13,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token1.type = TokenTypeEnum.TOTP token1.algorithm = TokenAlgorithmEnum.SHA1 - var url = otpService.tokenToOtpAuthUrl(token: token1)! + var url = token1.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") // Test case 2: TOTP token without account @@ -27,7 +25,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token2.type = TokenTypeEnum.TOTP token2.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token2)! + url = token2.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") // Test case 3: TOTP token with different period and algorithm @@ -40,7 +38,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token3.type = TokenTypeEnum.TOTP token3.algorithm = TokenAlgorithmEnum.SHA256 - url = otpService.tokenToOtpAuthUrl(token: token3)! + url = token3.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=Apple&period=45") // Test case 4: TOTP token without issuer @@ -52,7 +50,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token4.type = TokenTypeEnum.TOTP token4.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token4)! + url = token4.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/john@doe.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") // Test case 5: TOTP token with special characters in account and issuer @@ -65,7 +63,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token5.type = TokenTypeEnum.TOTP token5.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token5)! + url = token5.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/My%20Company:john.doe+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&period=30") // Test case 6: TOTP token with no issuer and no account @@ -76,7 +74,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token6.type = TokenTypeEnum.TOTP token6.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token6)! + url = token6.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") // Test case 7: TOTP token with different digits and SHA512 algorithm @@ -89,7 +87,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token7.type = TokenTypeEnum.TOTP token7.algorithm = TokenAlgorithmEnum.SHA512 - url = otpService.tokenToOtpAuthUrl(token: token7)! + url = token7.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/Google:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&period=60") // Test case 8: TOTP token with empty secret @@ -102,13 +100,11 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token8.type = TokenTypeEnum.TOTP token8.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token8)! + url = token8.otpAuthUrl()! XCTAssertEqual(url, "otpauth://totp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&period=30") } func testHotp() throws { - let otpService = OTPService() - // Test case 1: Standard HOTP token let token1 = Token() token1.issuer = "GitHub" @@ -119,7 +115,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token1.type = TokenTypeEnum.HOTP token1.algorithm = TokenAlgorithmEnum.SHA1 - var url = otpService.tokenToOtpAuthUrl(token: token1)! + var url = token1.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") // Test case 2: HOTP token without account @@ -131,7 +127,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token2.type = TokenTypeEnum.HOTP token2.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token2)! + url = token2.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") // Test case 3: HOTP token with different counter and algorithm @@ -144,7 +140,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token3.type = TokenTypeEnum.HOTP token3.algorithm = TokenAlgorithmEnum.SHA256 - url = otpService.tokenToOtpAuthUrl(token: token3)! + url = token3.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=GitHub&counter=10") // Test case 4: HOTP token without issuer @@ -156,7 +152,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token4.type = TokenTypeEnum.HOTP token4.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token4)! + url = token4.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") // Test case 5: HOTP token with special characters in account and issuer @@ -169,7 +165,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token5.type = TokenTypeEnum.HOTP token5.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token5)! + url = token5.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/My%20Company:user+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&counter=1") // Test case 6: HOTP token with no issuer and no account @@ -180,7 +176,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token6.type = TokenTypeEnum.HOTP token6.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token6)! + url = token6.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") // Test case 7: HOTP token with different digits and SHA512 algorithm @@ -193,7 +189,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token7.type = TokenTypeEnum.HOTP token7.algorithm = TokenAlgorithmEnum.SHA512 - url = otpService.tokenToOtpAuthUrl(token: token7)! + url = token7.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/Google:user@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&counter=100") // Test case 8: HOTP token with empty secret @@ -206,7 +202,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token8.type = TokenTypeEnum.HOTP token8.algorithm = TokenAlgorithmEnum.SHA1 - url = otpService.tokenToOtpAuthUrl(token: token8)! + url = token8.otpAuthUrl()! XCTAssertEqual(url, "otpauth://hotp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&counter=1") } } From 09ba64fa6c4894d729a2d5cd75a4b9bea69febd6 Mon Sep 17 00:00:00 2001 From: Joel-David Date: Mon, 7 Oct 2024 00:28:42 +0800 Subject: [PATCH 5/6] Added TokenValidator tests --- Chronos.xcodeproj/project.pbxproj | 4 + Chronos/Helper/OtpAuthUrlParser.swift | 25 ++++- Chronos/Helper/TokenValidator.swift | 20 ++-- ChronosTests/Token/ParseOtpAuthUrl.swift | 31 ++++- ChronosTests/Token/TokenValidator.swift | 137 +++++++++++++++++++++++ 5 files changed, 198 insertions(+), 19 deletions(-) create mode 100644 ChronosTests/Token/TokenValidator.swift diff --git a/Chronos.xcodeproj/project.pbxproj b/Chronos.xcodeproj/project.pbxproj index abc6cbe..7ae131e 100644 --- a/Chronos.xcodeproj/project.pbxproj +++ b/Chronos.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 6B09CEBF2CB2DEA10054AB61 /* TokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09CEBE2CB2DEA00054AB61 /* TokenValidator.swift */; }; 6B09CEC12CB2DF060054AB61 /* OtpAuthUrlParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09CEC02CB2DF060054AB61 /* OtpAuthUrlParser.swift */; }; + 6B09CEC32CB2EE9C0054AB61 /* TokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09CEC22CB2EE980054AB61 /* TokenValidator.swift */; }; 6B12B0A02C19DB7800E9ED2D /* ExportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B09F2C19DB7800E9ED2D /* ExportService.swift */; }; 6B12B0A22C19F1E600E9ED2D /* ExportVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */; }; 6B193C8A2C27F03300E759B7 /* CloudKitSyncMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 6B193C892C27F03300E759B7 /* CloudKitSyncMonitor */; }; @@ -97,6 +98,7 @@ /* Begin PBXFileReference section */ 6B09CEBE2CB2DEA00054AB61 /* TokenValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenValidator.swift; sourceTree = ""; }; 6B09CEC02CB2DF060054AB61 /* OtpAuthUrlParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtpAuthUrlParser.swift; sourceTree = ""; }; + 6B09CEC22CB2EE980054AB61 /* TokenValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenValidator.swift; sourceTree = ""; }; 6B12B09F2C19DB7800E9ED2D /* ExportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportService.swift; sourceTree = ""; }; 6B12B0A12C19F1E600E9ED2D /* ExportVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportVault.swift; sourceTree = ""; }; 6B19CA712B7A70B800690390 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; @@ -391,6 +393,7 @@ 6B8132F82C4C0F3B00DB367E /* Token */ = { isa = PBXGroup; children = ( + 6B09CEC22CB2EE980054AB61 /* TokenValidator.swift */, 6B8132F92C4C0F6300DB367E /* TokenToOtpAuthUrl.swift */, 6B8132FC2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift */, 6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */, @@ -709,6 +712,7 @@ 6B8132FD2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift in Sources */, 6BBF32032C562F20003CBA66 /* Aegis.swift in Sources */, 6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */, + 6B09CEC32CB2EE9C0054AB61 /* TokenValidator.swift in Sources */, 6B2F7A842C636CE500DB1450 /* Ente.swift in Sources */, 6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */, 6B4987282C569A6E00A7D97A /* LastPass.swift in Sources */, diff --git a/Chronos/Helper/OtpAuthUrlParser.swift b/Chronos/Helper/OtpAuthUrlParser.swift index 9792041..e5e80c6 100644 --- a/Chronos/Helper/OtpAuthUrlParser.swift +++ b/Chronos/Helper/OtpAuthUrlParser.swift @@ -1,6 +1,23 @@ import Foundation import SwiftOTP +public enum OtpAuthUrlError: LocalizedError, Equatable { + case invalidURL + case invalidQueryItem + case invalidType + + public var errorDescription: String? { + switch self { + case .invalidURL: + return "Invalid Otp Auth URL." + case .invalidQueryItem: + return "Invalid Query Item." + case .invalidType: + return "Invalid Type." + } + } +} + public class OtpAuthUrlParser { static func parseOtpAuthUrl(otpAuthStr: String) throws -> Token { guard let otpAuthURL = URL(string: otpAuthStr), @@ -8,7 +25,7 @@ public class OtpAuthUrlParser { let scheme = components.scheme, scheme == "otpauth", let host = components.host else { - throw OTPError.invalidURL + throw OtpAuthUrlError.invalidURL } let tokenType = try getTokenType(from: host) @@ -40,7 +57,7 @@ public class OtpAuthUrlParser { } } - guard let queryItems = components.queryItems else { throw OTPError.invalidQueryItem } + guard let queryItems = components.queryItems else { throw OtpAuthUrlError.invalidQueryItem } for item in queryItems { guard let value = item.value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { continue } switch item.name.lowercased() { @@ -50,7 +67,7 @@ public class OtpAuthUrlParser { token.issuer = value case "algorithm": guard let algo = TokenAlgorithmEnum(rawValue: value.uppercased()) else { - throw OTPError.invalidAlgorithm(value) + throw TokenError.invalidAlgorithm(value) } token.algorithm = algo case "digits": @@ -78,7 +95,7 @@ public class OtpAuthUrlParser { case "hotp": return .HOTP default: - throw OTPError.invalidType + throw OtpAuthUrlError.invalidType } } } diff --git a/Chronos/Helper/TokenValidator.swift b/Chronos/Helper/TokenValidator.swift index 5a7f239..45147f8 100644 --- a/Chronos/Helper/TokenValidator.swift +++ b/Chronos/Helper/TokenValidator.swift @@ -1,10 +1,8 @@ import Foundation import SwiftOTP -public enum OTPError: LocalizedError { - case invalidURL +public enum TokenError: LocalizedError, Equatable { case invalidType - case invalidQueryItem case invalidSecret(String) case invalidAlgorithm(String) case invalidDigits(Int) @@ -13,12 +11,8 @@ public enum OTPError: LocalizedError { public var errorDescription: String? { switch self { - case .invalidURL: - return "The provided URL is invalid." case .invalidType: return "The token type is unsupported." - case .invalidQueryItem: - return "The query item in the URL is invalid." case let .invalidSecret(reason): return "Invalid secret: \(reason)." case let .invalidAlgorithm(reason): @@ -37,34 +31,34 @@ public class TokenValidator { static func validate(token: Token) throws { // Validate Secret guard !token.secret.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - throw OTPError.invalidSecret("Secret cannot be empty.") + throw TokenError.invalidSecret("Secret cannot be empty.") } guard let _ = base32DecodeToData(token.secret) else { - throw OTPError.invalidSecret("Secret is not base32 encoded.") + throw TokenError.invalidSecret("Secret is not base32 encoded.") } // Validate Algorithm guard let _ = TokenAlgorithmEnum(rawValue: token.algorithm.rawValue) else { - throw OTPError.invalidAlgorithm("The provided algorithm is not valid.") + throw TokenError.invalidAlgorithm("The provided algorithm is not valid.") } // Validate Digits guard (6 ... 8).contains(token.digits) else { - throw OTPError.invalidDigits(token.digits) + throw TokenError.invalidDigits(token.digits) } // Validate Counter (specific to HOTP) if token.type == .HOTP { guard token.counter >= 0 else { - throw OTPError.invalidCounter(token.counter) + throw TokenError.invalidCounter(token.counter) } } // Validate Period (specific to TOTP) if token.type == .TOTP { guard token.period > 0 else { - throw OTPError.invalidPeriod(token.period) + throw TokenError.invalidPeriod(token.period) } } } diff --git a/ChronosTests/Token/ParseOtpAuthUrl.swift b/ChronosTests/Token/ParseOtpAuthUrl.swift index acb4fb2..9a25ae9 100644 --- a/ChronosTests/Token/ParseOtpAuthUrl.swift +++ b/ChronosTests/Token/ParseOtpAuthUrl.swift @@ -2,6 +2,33 @@ import XCTest final class ParseOtpAuthUrlTests: XCTestCase { + // Test invalid URL format + func testInvalidUrlFormat() { + let invalidUrl = "invalid-url-format" + + XCTAssertThrowsError(try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: invalidUrl)) { error in + XCTAssertEqual(error as? OtpAuthUrlError, .invalidURL) + } + } + + // Test invalid token type + func testInvalidTokenType() { + let invalidTokenTypeUrl = "otpauth://invalidtype/user@example.com?secret=JBSWY3DPEHPK3PXP" + + XCTAssertThrowsError(try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: invalidTokenTypeUrl)) { error in + XCTAssertEqual(error as? OtpAuthUrlError, .invalidType) + } + } + + // Test missing query items (e.g., missing secret) + func testMissingSecretQueryItem() { + let missingSecretUrl = "otpauth://totp/user@example.com" + + XCTAssertThrowsError(try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: missingSecretUrl)) { error in + XCTAssertEqual(error as? OtpAuthUrlError, .invalidQueryItem) + } + } + func testTotp() throws { // Test case 1: Standard TOTP token do { @@ -105,7 +132,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { do { _ = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&period=30") XCTFail("Expected OTPError.invalidSecret but no error was thrown") - } catch OTPError.invalidSecret { + } catch TokenError.invalidSecret { // Success: expected error was thrown } catch { XCTFail("Expected OTPError.invalidSecret but a different error was thrown") @@ -243,7 +270,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { do { _ = try OtpAuthUrlParser.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&counter=1") XCTFail("Expected OTPError.invalidSecret but no error was thrown") - } catch OTPError.invalidSecret { + } catch TokenError.invalidSecret { // Success: expected error was thrown } catch { XCTFail("Expected OTPError.invalidSecret but a different error was thrown") diff --git a/ChronosTests/Token/TokenValidator.swift b/ChronosTests/Token/TokenValidator.swift new file mode 100644 index 0000000..11be91b --- /dev/null +++ b/ChronosTests/Token/TokenValidator.swift @@ -0,0 +1,137 @@ +@testable import Chronos +import XCTest + +class TokenValidatorTests: XCTestCase { + + // Helper function to create a valid base32 secret + private func validBase32Secret() -> String { + return "JBSWY3DPEHPK3PXP" // A valid base32 encoded string + } + + // Test valid TOTP token + func testValidTOTPToken() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = 6 + token.period = 30 + + XCTAssertNoThrow(try TokenValidator.validate(token: token)) + } + + // Test valid HOTP token + func testValidHOTPToken() { + let token = Token() + token.secret = validBase32Secret() + token.type = .HOTP + token.digits = 6 + token.counter = 1 + + XCTAssertNoThrow(try TokenValidator.validate(token: token)) + } + + // Test empty secret + func testEmptySecret() { + let token = Token() + token.secret = "" + token.type = .TOTP + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidSecret("Secret cannot be empty.")) + } + } + + // Test invalid (non-base32) secret + func testInvalidSecret() { + let token = Token() + token.secret = "INVALID_SECRET" + token.type = .TOTP + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidSecret("Secret is not base32 encoded.")) + } + } + + // Test invalid digits (not in the range of 6-8) + func testInvalidDigitsUpper() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = 9 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(9)) + } + } + + func testInvalidDigitsLower() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = 5 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(5)) + } + } + + func testInvalidDigitsZero() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = 0 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(0)) + } + } + + func testInvalidDigitsNegative() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = -10 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(-10)) + } + } + + // Test negative counter for HOTP token + func testNegativeCounterForHOTP() { + let token = Token() + token.secret = validBase32Secret() + token.type = .HOTP + token.digits = 6 + token.counter = -1 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidCounter(-1)) + } + } + + // Test invalid period for TOTP token (must be greater than 0) + func testInvalidPeriodForTOTP() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = 6 + token.period = 0 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidPeriod(0)) + } + } + + func testInvalidPeriodForTOTPNegative() { + let token = Token() + token.secret = validBase32Secret() + token.type = .TOTP + token.digits = 6 + token.period = -10 + + XCTAssertThrowsError(try TokenValidator.validate(token: token)) { error in + XCTAssertEqual(error as? TokenError, TokenError.invalidPeriod(-10)) + } + } +} From ee7a58d4b2b509f7f06b6de751619218547eb600 Mon Sep 17 00:00:00 2001 From: Joel-David Date: Mon, 7 Oct 2024 00:29:16 +0800 Subject: [PATCH 6/6] Fixed lint --- Chronos/Helper/OtpAuthUrlParser.swift | 6 +++--- ChronosTests/Token/ParseOtpAuthUrl.swift | 2 +- ChronosTests/Token/TokenValidator.swift | 13 ++++++------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Chronos/Helper/OtpAuthUrlParser.swift b/Chronos/Helper/OtpAuthUrlParser.swift index e5e80c6..b4c532d 100644 --- a/Chronos/Helper/OtpAuthUrlParser.swift +++ b/Chronos/Helper/OtpAuthUrlParser.swift @@ -5,14 +5,14 @@ public enum OtpAuthUrlError: LocalizedError, Equatable { case invalidURL case invalidQueryItem case invalidType - + public var errorDescription: String? { switch self { case .invalidURL: return "Invalid Otp Auth URL." - case .invalidQueryItem: + case .invalidQueryItem: return "Invalid Query Item." - case .invalidType: + case .invalidType: return "Invalid Type." } } diff --git a/ChronosTests/Token/ParseOtpAuthUrl.swift b/ChronosTests/Token/ParseOtpAuthUrl.swift index 9a25ae9..922e988 100644 --- a/ChronosTests/Token/ParseOtpAuthUrl.swift +++ b/ChronosTests/Token/ParseOtpAuthUrl.swift @@ -28,7 +28,7 @@ final class ParseOtpAuthUrlTests: XCTestCase { XCTAssertEqual(error as? OtpAuthUrlError, .invalidQueryItem) } } - + func testTotp() throws { // Test case 1: Standard TOTP token do { diff --git a/ChronosTests/Token/TokenValidator.swift b/ChronosTests/Token/TokenValidator.swift index 11be91b..eba1608 100644 --- a/ChronosTests/Token/TokenValidator.swift +++ b/ChronosTests/Token/TokenValidator.swift @@ -2,10 +2,9 @@ import XCTest class TokenValidatorTests: XCTestCase { - // Helper function to create a valid base32 secret private func validBase32Secret() -> String { - return "JBSWY3DPEHPK3PXP" // A valid base32 encoded string + return "JBSWY3DPEHPK3PXP" // A valid base32 encoded string } // Test valid TOTP token @@ -63,7 +62,7 @@ class TokenValidatorTests: XCTestCase { XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(9)) } } - + func testInvalidDigitsLower() { let token = Token() token.secret = validBase32Secret() @@ -74,7 +73,7 @@ class TokenValidatorTests: XCTestCase { XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(5)) } } - + func testInvalidDigitsZero() { let token = Token() token.secret = validBase32Secret() @@ -85,7 +84,7 @@ class TokenValidatorTests: XCTestCase { XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(0)) } } - + func testInvalidDigitsNegative() { let token = Token() token.secret = validBase32Secret() @@ -96,7 +95,7 @@ class TokenValidatorTests: XCTestCase { XCTAssertEqual(error as? TokenError, TokenError.invalidDigits(-10)) } } - + // Test negative counter for HOTP token func testNegativeCounterForHOTP() { let token = Token() @@ -122,7 +121,7 @@ class TokenValidatorTests: XCTestCase { XCTAssertEqual(error as? TokenError, TokenError.invalidPeriod(0)) } } - + func testInvalidPeriodForTOTPNegative() { let token = Token() token.secret = validBase32Secret()