diff --git a/.codecov.yml b/.codecov.yml index 46fd814..dbb7318 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,7 +6,6 @@ codecov: ignore: - Tests - - OneTimePasswordLegacyTests coverage: status: diff --git a/OneTimePassword.xcodeproj/project.pbxproj b/OneTimePassword.xcodeproj/project.pbxproj index d0c19e9..7152e45 100644 --- a/OneTimePassword.xcodeproj/project.pbxproj +++ b/OneTimePassword.xcodeproj/project.pbxproj @@ -31,15 +31,10 @@ C9290C301947D104008AE4DE /* TokenSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9290C2F1947D104008AE4DE /* TokenSerializationTests.swift */; }; C93A251A196B1BA400F86892 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93A2519196B1BA400F86892 /* Token.swift */; }; C944A55F1A7EDAE200E08B1E /* Base32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C944A55E1A7EDAE200E08B1E /* Base32.framework */; }; - C944A5951A809CC000E08B1E /* OTPTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C944A5941A809CC000E08B1E /* OTPTokenTests.swift */; }; C94B2007197774A20014A202 /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94B2006197774A20014A202 /* TokenTests.swift */; }; C95B10CC196D22B9000840AA /* GeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95B10CB196D22B9000840AA /* GeneratorTests.swift */; }; C95F9FB91C03D6BC00CEA286 /* PersistentToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95F9FB81C03D6BC00CEA286 /* PersistentToken.swift */; }; - C97142361C1155FC0063B37E /* OTPToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DC7ECC196C4D3D00B50C82 /* OTPToken.swift */; }; - C97142371C1156DB0063B37E /* OneTimePassword.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C97C82381946E51D00FD9F4C /* OneTimePassword.framework */; }; C97C82441946E51D00FD9F4C /* OneTimePassword.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C97C82381946E51D00FD9F4C /* OneTimePassword.framework */; }; - C9A486C8196F38C800524F51 /* OTPTokenSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C93A2515196AFE1100F86892 /* OTPTokenSerializationTests.m */; settings = {COMPILER_FLAGS = "-Wno-nullable-to-nonnull-conversion"; }; }; - C9A486C9196F38CD00524F51 /* OTPTypeStrings.m in Sources */ = {isa = PBXBuildFile; fileRef = C9C9FE25196D181800C7ACEE /* OTPTypeStrings.m */; }; C9B2A19C199A7F1B00BC4A8A /* EquatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B2A19B199A7F1B00BC4A8A /* EquatableTests.swift */; }; C9B77D771C03078B00BAF6BF /* KeychainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93A2514196AFE1100F86892 /* KeychainTests.swift */; }; C9DC7EC4196BD5DF00B50C82 /* Token+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DC7EC3196BD5DF00B50C82 /* Token+URL.swift */; }; @@ -50,13 +45,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - C97142381C1157FC0063B37E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C97C822F1946E51D00FD9F4C /* Project object */; - proxyType = 1; - remoteGlobalIDString = C97C82371946E51D00FD9F4C; - remoteInfo = "OneTimePassword (iOS)"; - }; C97C82451946E51D00FD9F4C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C97C822F1946E51D00FD9F4C /* Project object */; @@ -100,7 +88,6 @@ C9003417196F7046009733E8 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; C9290C2F1947D104008AE4DE /* TokenSerializationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSerializationTests.swift; sourceTree = ""; }; C93A2514196AFE1100F86892 /* KeychainTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainTests.swift; sourceTree = ""; }; - C93A2515196AFE1100F86892 /* OTPTokenSerializationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OTPTokenSerializationTests.m; sourceTree = ""; }; C93A2519196B1BA400F86892 /* Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; C93CC01A1DCBB755006255FA /* OneTimePassword-iOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "OneTimePassword-iOS.xcconfig"; sourceTree = ""; }; C93CC01B1DCBB7FB006255FA /* OneTimePassword-watchOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "OneTimePassword-watchOS.xcconfig"; sourceTree = ""; }; @@ -108,7 +95,6 @@ C93CC01E1DCBBDE7006255FA /* OneTimePassword.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = OneTimePassword.xcconfig; sourceTree = ""; }; C93CC0211DCBC189006255FA /* OneTimePasswordTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = OneTimePasswordTests.xcconfig; sourceTree = ""; }; C944A55E1A7EDAE200E08B1E /* Base32.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Base32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C944A5941A809CC000E08B1E /* OTPTokenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPTokenTests.swift; sourceTree = ""; }; C94765061C64587800C7527E /* Cartfile.private */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.private; sourceTree = ""; }; C94B2006197774A20014A202 /* TokenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenTests.swift; sourceTree = ""; }; C94B9BC81BD7270E0073D7C5 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; @@ -125,17 +111,12 @@ C996EC2D1A74D5830076B105 /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Configurations/Profile.xcconfig; sourceTree = ""; }; C996EC2E1A74D5830076B105 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Configurations/Release.xcconfig; sourceTree = ""; }; C996EC2F1A74D5830076B105 /* Test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Test.xcconfig; path = Configurations/Test.xcconfig; sourceTree = ""; }; - C9A486B3196F352E00524F51 /* OneTimePasswordLegacyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneTimePasswordLegacyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - C9A486B9196F352F00524F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C9B2A19B199A7F1B00BC4A8A /* EquatableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EquatableTests.swift; sourceTree = ""; }; C9B84D1C1C015EC0002EE631 /* .hound.yml */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = text; path = .hound.yml; sourceTree = ""; }; C9B84D1D1C015EC0002EE631 /* .swiftlint.yml */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; C9B84D1F1C015EC8002EE631 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; - C9C9FE24196D181800C7ACEE /* OTPTypeStrings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OTPTypeStrings.h; sourceTree = ""; }; - C9C9FE25196D181800C7ACEE /* OTPTypeStrings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OTPTypeStrings.m; sourceTree = ""; }; C9DC7EC3196BD5DF00B50C82 /* Token+URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Token+URL.swift"; sourceTree = ""; }; C9DC7EC7196BDF3B00B50C82 /* Generator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Generator.swift; sourceTree = ""; }; - C9DC7ECC196C4D3D00B50C82 /* OTPToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPToken.swift; sourceTree = ""; }; C9E829531C62DFDA003F5FC9 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; C9E829541C62FFBD003F5FC9 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; C9E829551C630514003F5FC9 /* CONDUCT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONDUCT.md; sourceTree = ""; }; @@ -171,14 +152,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C9A486B0196F352E00524F51 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C97142371C1156DB0063B37E /* OneTimePassword.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -229,7 +202,6 @@ children = ( C97C823A1946E51D00FD9F4C /* Sources */, C97C82471946E51D00FD9F4C /* Tests */, - C9A486B7196F352F00524F51 /* OneTimePasswordLegacyTests */, 746E7166AD60449882DD84C7 /* Frameworks */, C996EC281A74D5830076B105 /* Configuration */, C97C82391946E51D00FD9F4C /* Products */, @@ -245,7 +217,6 @@ children = ( C97C82381946E51D00FD9F4C /* OneTimePassword.framework */, C97C82431946E51D00FD9F4C /* OneTimePasswordTests.xctest */, - C9A486B3196F352E00524F51 /* OneTimePasswordLegacyTests.xctest */, 5B39F4941DBD06BA00CD2DAB /* OneTimePassword.framework */, FD6C3C0C1E0200F800EC4528 /* OneTimePasswordTestApp.app */, ); @@ -315,26 +286,6 @@ path = Carthage/Checkouts/xcconfigs/Base; sourceTree = SOURCE_ROOT; }; - C9A486B7196F352F00524F51 /* OneTimePasswordLegacyTests */ = { - isa = PBXGroup; - children = ( - C9DC7ECC196C4D3D00B50C82 /* OTPToken.swift */, - C944A5941A809CC000E08B1E /* OTPTokenTests.swift */, - C93A2515196AFE1100F86892 /* OTPTokenSerializationTests.m */, - C9C9FE23196D176300C7ACEE /* Helpers */, - C9A486B8196F352F00524F51 /* Supporting Files */, - ); - path = OneTimePasswordLegacyTests; - sourceTree = ""; - }; - C9A486B8196F352F00524F51 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - C9A486B9196F352F00524F51 /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; C9A9B09A1A81EF4B00F3C4DC /* Persistence */ = { isa = PBXGroup; children = ( @@ -357,15 +308,6 @@ name = Tools; sourceTree = ""; }; - C9C9FE23196D176300C7ACEE /* Helpers */ = { - isa = PBXGroup; - children = ( - C9C9FE24196D181800C7ACEE /* OTPTypeStrings.h */, - C9C9FE25196D181800C7ACEE /* OTPTypeStrings.m */, - ); - name = Helpers; - sourceTree = ""; - }; FD6C3C0D1E0200F800EC4528 /* Test App */ = { isa = PBXGroup; children = ( @@ -430,23 +372,6 @@ productReference = C97C82431946E51D00FD9F4C /* OneTimePasswordTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - C9A486B2196F352E00524F51 /* OneTimePasswordLegacyTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = C9A486C1196F352F00524F51 /* Build configuration list for PBXNativeTarget "OneTimePasswordLegacyTests" */; - buildPhases = ( - C9A486AF196F352E00524F51 /* Sources */, - C9A486B0196F352E00524F51 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - C97142391C1157FC0063B37E /* PBXTargetDependency */, - ); - name = OneTimePasswordLegacyTests; - productName = OneTimePasswordLegacyTests; - productReference = C9A486B3196F352E00524F51 /* OneTimePasswordLegacyTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; FD6C3C0B1E0200F800EC4528 /* OneTimePasswordTestApp */ = { isa = PBXNativeTarget; buildConfigurationList = FD6C3C2A1E0200F900EC4528 /* Build configuration list for PBXNativeTarget "OneTimePasswordTestApp" */; @@ -495,11 +420,6 @@ ProvisioningStyle = Manual; TestTargetID = FD6C3C0B1E0200F800EC4528; }; - C9A486B2196F352E00524F51 = { - CreatedOnToolsVersion = 6.0; - LastSwiftMigration = 1020; - ProvisioningStyle = Manual; - }; FD6C3C0B1E0200F800EC4528 = { CreatedOnToolsVersion = 8.2; LastSwiftMigration = 1020; @@ -522,7 +442,6 @@ targets = ( C97C82371946E51D00FD9F4C /* OneTimePassword (iOS) */, C97C82421946E51D00FD9F4C /* OneTimePasswordTests */, - C9A486B2196F352E00524F51 /* OneTimePasswordLegacyTests */, FD6C3C0B1E0200F800EC4528 /* OneTimePasswordTestApp */, 5B39F4931DBD06BA00CD2DAB /* OneTimePassword (watchOS) */, C9425DE4227501F500EF93BD /* Lint OneTimePassword */, @@ -600,17 +519,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C9A486AF196F352E00524F51 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C9A486C8196F38C800524F51 /* OTPTokenSerializationTests.m in Sources */, - C97142361C1155FC0063B37E /* OTPToken.swift in Sources */, - C9A486C9196F38CD00524F51 /* OTPTypeStrings.m in Sources */, - C944A5951A809CC000E08B1E /* OTPTokenTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; FD6C3C081E0200F800EC4528 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -622,11 +530,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - C97142391C1157FC0063B37E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C97C82371946E51D00FD9F4C /* OneTimePassword (iOS) */; - targetProxy = C97142381C1157FC0063B37E /* PBXContainerItemProxy */; - }; C97C82461946E51D00FD9F4C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C97C82371946E51D00FD9F4C /* OneTimePassword (iOS) */; @@ -725,26 +628,6 @@ }; name = Release; }; - C9A486BE196F352F00524F51 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C93CC01C1DCBB875006255FA /* OneTimePasswordTests-iOS.xcconfig */; - buildSettings = { - INFOPLIST_FILE = OneTimePasswordLegacyTests/Info.plist; - PRODUCT_BUNDLE_IDENTIFIER = me.mattrubin.onetimepassword.legacy.tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - C9A486BF196F352F00524F51 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C93CC01C1DCBB875006255FA /* OneTimePasswordTests-iOS.xcconfig */; - buildSettings = { - INFOPLIST_FILE = OneTimePasswordLegacyTests/Info.plist; - PRODUCT_BUNDLE_IDENTIFIER = me.mattrubin.onetimepassword.legacy.tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; FD6C3C261E0200F900EC4528 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = FDA64C771E021394004AD993 /* OneTimePasswordTestApp.xcconfig */; @@ -807,15 +690,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C9A486C1196F352F00524F51 /* Build configuration list for PBXNativeTarget "OneTimePasswordLegacyTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C9A486BE196F352F00524F51 /* Debug */, - C9A486BF196F352F00524F51 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; FD6C3C2A1E0200F900EC4528 /* Build configuration list for PBXNativeTarget "OneTimePasswordTestApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/OneTimePassword.xcodeproj/xcshareddata/xcschemes/OneTimePassword (iOS).xcscheme b/OneTimePassword.xcodeproj/xcshareddata/xcschemes/OneTimePassword (iOS).xcscheme index 3dbe48c..e151206 100644 --- a/OneTimePassword.xcodeproj/xcshareddata/xcschemes/OneTimePassword (iOS).xcscheme +++ b/OneTimePassword.xcodeproj/xcshareddata/xcschemes/OneTimePassword (iOS).xcscheme @@ -62,16 +62,6 @@ ReferencedContainer = "container:OneTimePassword.xcodeproj"> - - - - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/OneTimePasswordLegacyTests/OTPToken.swift b/OneTimePasswordLegacyTests/OTPToken.swift deleted file mode 100644 index adc4ebc..0000000 --- a/OneTimePasswordLegacyTests/OTPToken.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// OTPToken.swift -// OneTimePassword -// -// Copyright (c) 2013-2018 Matt Rubin and the OneTimePassword authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Foundation -import OneTimePassword - -/// `OTPToken` is a mutable, Objective-C-compatible wrapper around `OneTimePassword.Token`. For more -/// information about its properties and methods, consult the underlying `OneTimePassword` -/// documentation. -public final class OTPToken: NSObject { - override public required init() {} - - @objc public var name: String = OTPToken.defaultName - @objc public var issuer: String = OTPToken.defaultIssuer - @objc public var type: OTPTokenType = .timer - @objc public var secret: Data = Data() - @objc public var algorithm: OTPAlgorithm = OTPToken.defaultAlgorithm - @objc public var digits: UInt = OTPToken.defaultDigits - @objc public var period: TimeInterval = OTPToken.defaultPeriod - @objc public var counter: UInt64 = OTPToken.defaultInitialCounter - - private static let defaultName: String = "" - private static let defaultIssuer: String = "" - private static let defaultAlgorithm: OTPAlgorithm = .sha1 - private static var defaultDigits: UInt = 6 - private static var defaultInitialCounter: UInt64 = 0 - private static var defaultPeriod: TimeInterval = 30 - - private func update(with token: Token) { - self.name = token.name - self.issuer = token.issuer - - self.secret = token.generator.secret - self.algorithm = OTPAlgorithm(token.generator.algorithm) - self.digits = UInt(token.generator.digits) - - switch token.generator.factor { - case let .counter(counter): - self.type = .counter - self.counter = counter - case let .timer(period): - self.type = .timer - self.period = period - } - } - - private convenience init(token: Token) { - self.init() - update(with: token) - } - - @objc - public func validate() -> Bool { - return (tokenForOTPToken(self) != nil) - } -} - -public extension OTPToken { - @objc(tokenWithURL:) - static func token(from url: URL) -> Self? { - return token(from: url, secret: nil) - } - - @objc(tokenWithURL:secret:) - static func token(from url: URL, secret: Data?) -> Self? { - guard let token = try? Token(url: url, secret: secret) else { - return nil - } - return self.init(token: token) - } - - @objc - func url() -> URL? { - guard let token = tokenForOTPToken(self) else { - return nil - } - return try? token.toURL() - } -} - -// MARK: Enums - -// swiftlint:disable explicit_enum_raw_value -@objc -public enum OTPTokenType: UInt8 { - case counter - case timer -} - -@objc -public enum OTPAlgorithm: UInt32 { - @objc(OTPAlgorithmSHA1) case sha1 - @objc(OTPAlgorithmSHA256) case sha256 - @objc(OTPAlgorithmSHA512) case sha512 -} -// swiftlint:enable explicit_enum_raw_value - -// MARK: Conversion - -private extension OTPAlgorithm { - init(_ generatorAlgorithm: Generator.Algorithm) { - switch generatorAlgorithm { - case .sha1: - self = .sha1 - case .sha256: - self = .sha256 - case .sha512: - self = .sha512 - } - } -} - -private func tokenForOTPToken(_ otpToken: OTPToken) -> Token? { - guard let generator = try? Generator( - factor: factorForOTPToken(otpToken), - secret: otpToken.secret, - algorithm: algorithmForOTPAlgorithm(otpToken.algorithm), - digits: Int(otpToken.digits) - ) else { - return nil - } - return Token(name: otpToken.name, issuer: otpToken.issuer, generator: generator) -} - -private func factorForOTPToken(_ otpToken: OTPToken) -> Generator.Factor { - switch otpToken.type { - case .counter: - return .counter(otpToken.counter) - case .timer: - return .timer(period: otpToken.period) - } -} - -private func algorithmForOTPAlgorithm(_ algorithm: OTPAlgorithm) -> Generator.Algorithm { - switch algorithm { - case .sha1: - return .sha1 - case .sha256: - return .sha256 - case .sha512: - return .sha512 - } -} diff --git a/OneTimePasswordLegacyTests/OTPTokenSerializationTests.m b/OneTimePasswordLegacyTests/OTPTokenSerializationTests.m deleted file mode 100644 index 272f8fd..0000000 --- a/OneTimePasswordLegacyTests/OTPTokenSerializationTests.m +++ /dev/null @@ -1,475 +0,0 @@ -// -// OTPTokenSerializationTests.m -// OneTimePassword -// -// Copyright (c) 2013-2017 Matt Rubin and the OneTimePassword authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@import XCTest; -@import Base32; -#import "OneTimePasswordLegacyTests-Swift.h" -#import "OTPTypeStrings.h" - - -static NSString * const kOTPScheme = @"otpauth"; -static NSString * const kOTPTokenTypeCounterHost = @"hotp"; -static NSString * const kOTPTokenTypeTimerHost = @"totp"; -static NSString * const kRandomKey = @"RANDOM"; - -static NSArray *typeNumbers; -static NSArray *names; -static NSArray *issuers; -static NSArray *secretStrings; -static NSArray *algorithmNumbers; -static NSArray *digitNumbers; -static NSArray *periodNumbers; -static NSArray *counterNumbers; - -static const unsigned char kValidSecret[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; - - -@interface OTPTokenSerializationTests : XCTestCase - -@end - - -@implementation OTPTokenSerializationTests - -+ (void)setUp -{ - [super setUp]; - - typeNumbers = @[@(OTPTokenTypeCounter), @(OTPTokenTypeTimer)]; - names = @[@"", @"Login", @"user123@website.com", @"Léon", @":/?#[]@!$&'()*+,;=%\""]; - issuers = @[@"", @"Big Cörpøráçìôn", @":/?#[]@!$&'()*+,;=%\""]; - secretStrings = @[@"12345678901234567890", @"12345678901234567890123456789012", - @"1234567890123456789012345678901234567890123456789012345678901234", @""]; - algorithmNumbers = @[@(OTPAlgorithmSHA1), @(OTPAlgorithmSHA256), @(OTPAlgorithmSHA512)]; - digitNumbers = @[@0, @6, @8]; - periodNumbers = @[@0, @1, @30, kRandomKey]; - counterNumbers = @[@0, @1, @99999, kRandomKey]; -} - -#pragma mark - Brute Force Tests - -- (void)testDeserialization -{ - for (NSNumber *typeNumber in typeNumbers) { - for (NSString *name in names) { - for (NSString *issuer in issuers) { - for (NSString *secretString in secretStrings) { - for (NSNumber *algorithmNumber in algorithmNumbers) { - for (NSNumber *digitNumber in digitNumbers) { - for (NSNumber *periodNumber in periodNumbers) { - for (NSNumber *counterNumber in counterNumbers) { - // Construct the URL - NSURLComponents *urlComponents = [NSURLComponents new]; - urlComponents.scheme = kOTPScheme; - urlComponents.host = [NSString stringForTokenType:[typeNumber unsignedCharValue]]; - urlComponents.path = [@"/" stringByAppendingString:name]; - - NSMutableArray *queryItems = [NSMutableArray array]; - NSString *algorithmValue = [NSString stringForAlgorithm:[algorithmNumber unsignedIntValue]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"algorithm" - value:algorithmValue]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"digits" - value:[digitNumber stringValue]]]; - NSString *secretValue = [[[secretString dataUsingEncoding:NSASCIIStringEncoding] base32String] stringByReplacingOccurrencesOfString:@"=" withString:@""]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"secret" - value:secretValue]]; - NSNumber *periodValue = [periodNumber isEqual:kRandomKey] ? @(arc4random()%299 + 1) : periodNumber; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"period" - value:[periodValue stringValue]]]; - NSNumber *counterValue = [counterNumber isEqual:kRandomKey] ? @(arc4random() + ((uint64_t)arc4random() << 32)) : counterNumber; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"counter" - value:[counterValue stringValue]]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"issuer" - value:issuer]]; - urlComponents.queryItems = queryItems; - - // Create the token - OTPToken *token = [OTPToken tokenWithURL:[urlComponents URL]]; - - // Note: [OTPToken tokenWithURL:] will return nil if the token described by the URL is invalid. - if (token) { - XCTAssertEqual(token.type, [typeNumber unsignedCharValue], @"Incorrect token type"); - XCTAssertEqualObjects(token.name, name, @"Incorrect token name"); - XCTAssertEqualObjects(token.issuer, issuer, @"Incorrect token issuer"); - XCTAssertEqualObjects(token.secret, [secretString dataUsingEncoding:NSASCIIStringEncoding], @"Incorrect token secret"); - XCTAssertEqual(token.algorithm, [algorithmNumber unsignedIntValue], @"Incorrect token algorithm"); - XCTAssertEqual(token.digits, [digitNumber unsignedIntegerValue], @"Incorrect token digits"); - if (token.type == OTPTokenTypeTimer) - XCTAssertTrue(ABS(token.period - [periodValue doubleValue]) < DBL_EPSILON, @"Incorrect token period"); - if (token.type == OTPTokenTypeCounter) - XCTAssertEqual(token.counter, [counterValue unsignedLongLongValue], @"Incorrect token counter"); - } else { - // If nil was returned from [OTPToken tokenWithURL:], create the same token manually and ensure it's invalid - OTPToken *invalidToken = [OTPToken new]; - invalidToken.type = [typeNumber unsignedCharValue]; - invalidToken.name = name; - invalidToken.issuer = issuer; - invalidToken.secret = [secretString dataUsingEncoding:NSASCIIStringEncoding]; - invalidToken.algorithm = [algorithmNumber unsignedIntValue]; - invalidToken.digits = [digitNumber unsignedIntegerValue]; - invalidToken.period = [periodValue doubleValue]; - invalidToken.counter = [counterValue unsignedLongLongValue]; - - XCTAssertFalse([invalidToken validate], @"The token should be invalid"); - } - } - } - } - } - } - } - } - } -} - -- (void)testTokenWithURLAndSecret -{ - for (NSNumber *typeNumber in typeNumbers) { - for (NSString *name in names) { - for (NSString *issuer in issuers) { - for (NSString *secretString in secretStrings) { - for (NSNumber *algorithmNumber in algorithmNumbers) { - for (NSNumber *digitNumber in digitNumbers) { - for (NSNumber *periodNumber in periodNumbers) { - for (NSNumber *counterNumber in counterNumbers) { - // Construct the URL - NSURLComponents *urlComponents = [NSURLComponents new]; - urlComponents.scheme = kOTPScheme; - urlComponents.host = [NSString stringForTokenType:[typeNumber unsignedCharValue]]; - urlComponents.path = [@"/" stringByAppendingString:name]; - - NSMutableArray *queryItems = [NSMutableArray array]; - NSString *algorithmValue = [NSString stringForAlgorithm:[algorithmNumber unsignedIntValue]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"algorithm" - value:algorithmValue]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"digits" - value:[digitNumber stringValue]]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"secret" - value:@"A"]]; - NSNumber *periodValue = [periodNumber isEqual:kRandomKey] ? @(arc4random()%299 + 1) : periodNumber; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"period" - value:[periodValue stringValue]]]; - NSNumber *counterValue = [counterNumber isEqual:kRandomKey] ? @(arc4random() + ((uint64_t)arc4random() << 32)) : counterNumber; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"counter" - value:[counterValue stringValue]]]; - [queryItems addObject:[NSURLQueryItem queryItemWithName:@"issuer" - value:issuer]]; - urlComponents.queryItems = queryItems; - - // Create the token - NSData *secret = [secretString dataUsingEncoding:NSASCIIStringEncoding]; - OTPToken *token = [OTPToken tokenWithURL:[urlComponents URL] secret:secret]; - - // Note: [OTPToken tokenWithURL:] will return nil if the token described by the URL is invalid. - if (token) { - XCTAssertEqual(token.type, [typeNumber unsignedCharValue], @"Incorrect token type"); - XCTAssertEqualObjects(token.name, name, @"Incorrect token name"); - XCTAssertEqualObjects(token.issuer, issuer, @"Incorrect token issuer"); - XCTAssertEqualObjects(token.secret, secret, @"Incorrect token secret"); - XCTAssertEqual(token.algorithm, [algorithmNumber unsignedIntValue], @"Incorrect token algorithm"); - XCTAssertEqual(token.digits, [digitNumber unsignedIntegerValue], @"Incorrect token digits"); - if (token.type == OTPTokenTypeTimer) - XCTAssertTrue(ABS(token.period - [periodValue doubleValue]) < DBL_EPSILON, @"Incorrect token period"); - if (token.type == OTPTokenTypeCounter) - XCTAssertEqual(token.counter, [counterValue unsignedLongLongValue], @"Incorrect token counter"); - } else { - // If nil was returned from [OTPToken tokenWithURL:], create the same token manually and ensure it's invalid - OTPToken *invalidToken = [OTPToken new]; - invalidToken.type = [typeNumber unsignedCharValue]; - invalidToken.name = name; - invalidToken.issuer = issuer; - invalidToken.secret = secret; - invalidToken.algorithm = [algorithmNumber unsignedIntValue]; - invalidToken.digits = [digitNumber unsignedIntegerValue]; - invalidToken.period = [periodValue doubleValue]; - invalidToken.counter = [counterValue unsignedLongLongValue]; - - XCTAssertFalse([invalidToken validate], @"The token should be invalid"); - } - } - } - } - } - } - } - } - } -} - -- (void)testSerialization -{ - for (NSNumber *typeNumber in typeNumbers) { - for (NSString *name in names) { - for (NSString *issuer in issuers) { - for (NSString *secretString in secretStrings) { - for (NSNumber *algorithmNumber in algorithmNumbers) { - for (NSNumber *digitNumber in digitNumbers) { - for (NSNumber *periodNumber in periodNumbers) { - for (NSNumber *counterNumber in counterNumbers) { - - NSTimeInterval period; - if ([periodNumber isEqual:kRandomKey]) { - period = (arc4random()%299 + 1); - } else { - period = [periodNumber doubleValue]; - } - - uint64_t counter; - if ([counterNumber isEqual:kRandomKey]) { - counter = arc4random() + ((uint64_t)arc4random() << 32); - } else { - counter = [counterNumber unsignedLongLongValue]; - } - - // Create the token - OTPToken *token = [OTPToken new]; - token.type = [typeNumber unsignedCharValue]; - token.name = name; - token.secret = [secretString dataUsingEncoding:NSASCIIStringEncoding]; - token.algorithm = [algorithmNumber unsignedIntValue]; - token.digits = [digitNumber unsignedIntegerValue]; - token.period = period; - token.counter = counter; - token.issuer = issuer; - - // Serialize - NSURL *url = token.url; - - // An invalid token should not (cannot) produce a URL - if (![token validate]) { - XCTAssertNil(url); - continue; - } - - // Test scheme - XCTAssertEqualObjects(url.scheme, kOTPScheme, - @"The url scheme should be \"%@\"", kOTPScheme); - // Test type - NSString *expectedHost = [typeNumber unsignedCharValue] == OTPTokenTypeCounter ? kOTPTokenTypeCounterHost : kOTPTokenTypeTimerHost; - XCTAssertEqualObjects(url.host, expectedHost, - @"The url host should be \"%@\"", expectedHost); - // Test name - if (name) { - XCTAssertEqualObjects([url.path substringFromIndex:1] , name, - @"The url path should be \"%@\"", name); - } else { - XCTAssertEqualObjects(url.path, @"", @"The url path should be empty"); - } - - NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url - resolvingAgainstBaseURL:NO]; - NSArray *queryItems = urlComponents.queryItems; - NSMutableDictionary *queryArguments = [NSMutableDictionary dictionaryWithCapacity:queryItems.count]; - for (NSURLQueryItem *queryItem in queryItems) { - XCTAssertNil([queryArguments objectForKey:queryItem.name]); - [queryArguments setObject:queryItem.value forKey:queryItem.name]; - } - - // Test algorithm - NSString *expectedAlgorithmString = [NSString stringForAlgorithm:[algorithmNumber unsignedIntValue]]; - XCTAssertEqualObjects(queryArguments[@"algorithm"], expectedAlgorithmString, - @"The algorithm value should be \"%@\"", expectedAlgorithmString); - // Test digits - NSString *expectedDigitsString = [digitNumber stringValue]; - XCTAssertEqualObjects(queryArguments[@"digits"], expectedDigitsString, - @"The digits value should be \"%@\"", expectedDigitsString); - // Test secret - XCTAssertNil(queryArguments[@"secret"], @"The url query string should not contain the secret"); - - // Test period - if ([typeNumber unsignedCharValue] == OTPTokenTypeTimer) { - NSString *expectedPeriodString = [@(period) stringValue]; - XCTAssertEqualObjects(queryArguments[@"period"], expectedPeriodString, - @"The period value should be \"%@\"", expectedPeriodString); - } else { - XCTAssertNil(queryArguments[@"period"], @"The url query string should not contain the period"); - } - // Test counter - if ([typeNumber unsignedCharValue] == OTPTokenTypeCounter) { - NSString *expectedCounterString = [@(counter) stringValue]; - XCTAssertEqualObjects(queryArguments[@"counter"], expectedCounterString, - @"The counter value should be \"%@\"", expectedCounterString); - } else { - XCTAssertNil(queryArguments[@"counter"], @"The url query string should not contain the counter"); - } - - // Test issuer - XCTAssertEqualObjects(queryArguments[@"issuer"], issuer, - @"The issuer value should be \"%@\"", issuer); - - XCTAssertEqual(queryArguments.count, (NSUInteger)(issuer ? 4 : 3), @"There shouldn't be any unexpected query arguments"); - - // Check url again - NSURL *checkURL = token.url; - XCTAssertEqualObjects(url, checkURL, @"Repeated calls to -url should return the same result!"); - } - } - } - } - } - } - } - } -} - - -#pragma mark - Test with specific URLs -// From Google Authenticator for iOS -// https://code.google.com/p/google-authenticator/source/browse/mobile/ios/Classes/OTPAuthURLTest.m - -#pragma mark Deserialization - -- (void)testTokenWithTOTPURL -{ - NSData *secret = [NSData dataWithBytes:kValidSecret length:sizeof(kValidSecret)]; - OTPToken *token = [OTPToken tokenWithURL:[NSURL URLWithString:@"otpauth://totp/L%C3%A9on?algorithm=SHA256&digits=8&period=45&secret=AAAQEAYEAUDAOCAJBIFQYDIOB4"]]; - - XCTAssertEqualObjects(token.name, @"Léon"); - XCTAssertEqualObjects(token.secret, secret); - XCTAssertEqual(token.type, OTPTokenTypeTimer); - XCTAssertEqual(token.algorithm, OTPAlgorithmSHA256); - XCTAssertTrue(ABS(token.period - 45.0) < DBL_EPSILON); - XCTAssertEqual(token.digits, 8U); -} - -- (void)testTokenWithHOTPURL -{ - NSData *secret = [NSData dataWithBytes:kValidSecret length:sizeof(kValidSecret)]; - OTPToken *token = [OTPToken tokenWithURL:[NSURL URLWithString:@"otpauth://hotp/L%C3%A9on?algorithm=SHA256&digits=8&counter=18446744073709551615&secret=AAAQEAYEAUDAOCAJBIFQYDIOB4"]]; - - XCTAssertEqualObjects(token.name, @"Léon"); - XCTAssertEqualObjects(token.secret, secret); - XCTAssertEqual(token.type, OTPTokenTypeCounter); - XCTAssertEqual(token.algorithm, OTPAlgorithmSHA256); - XCTAssertEqual(token.counter, 18446744073709551615ULL); - XCTAssertEqual(token.digits, 8U); -} - -- (void)testTokenWithInvalidURLs -{ - NSArray *badURLs = @[@"http://foo", // invalid scheme - @"otpauth://foo", // invalid type - @"otpauth:///bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4", // missing type - @"otpauth://totp/bar", // missing secret - @"otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&period=0", // invalid period - @"otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&period=x", // non-numeric period - @"otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&period=30&period=60", // multiple period - @"otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&algorithm=MD5", // invalid algorithm - @"otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&digits=2", // invalid digits - @"otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&digits=x", // non-numeric digits - @"otpauth://hotp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&counter=1.5", // invalid counter - @"otpauth://hotp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&counter=x", // non-numeric counter - ]; - - for (NSString *badURL in badURLs) { - OTPToken *token = [OTPToken tokenWithURL:[NSURL URLWithString:badURL]]; - XCTAssertNil(token, @"Invalid url (%@) generated %@", badURL, token); - } -} - -- (void)testTokenWithIssuer -{ - OTPToken *simpleToken = [OTPToken tokenWithURL:[NSURL URLWithString:@"otpauth://totp/name?secret=A&issuer=issuer"]]; - XCTAssertNotNil(simpleToken); - XCTAssertEqualObjects(simpleToken.name, @"name"); - XCTAssertEqualObjects(simpleToken.issuer, @"issuer"); - - // TODO: test this more thoroughly, including the override case with "otpauth://totp/_issuer:name?secret=A&isser=issuer" - - NSArray *urlStrings = @[@"otpauth://totp/issu%C3%A9r%20!:name?secret=A", - @"otpauth://totp/issu%C3%A9r%20!:%20name?secret=A", - @"otpauth://totp/issu%C3%A9r%20!:%20%20%20name?secret=A", - @"otpauth://totp/issu%C3%A9r%20!%3Aname?secret=A", - @"otpauth://totp/issu%C3%A9r%20!%3A%20name?secret=A", - @"otpauth://totp/issu%C3%A9r%20!%3A%20%20%20name?secret=A", - ]; - for (NSString *urlString in urlStrings) { - // If there is no issuer argument, extract the issuer from the name - OTPToken *token = [OTPToken tokenWithURL:[NSURL URLWithString:urlString]]; - - XCTAssertNotNil(token, @"<%@> did not create a valid token.", urlString); - XCTAssertEqualObjects(token.name, @"name"); - XCTAssertEqualObjects(token.issuer, @"issuér !"); - - // If there is an issuer argument which matches the one in the name, trim the name - OTPToken *token2 = [OTPToken tokenWithURL:[NSURL URLWithString:[urlString stringByAppendingString:@"&issuer=issu%C3%A9r%20!"]]]; - - XCTAssertNotNil(token2, @"<%@> did not create a valid token.", urlString); - XCTAssertEqualObjects(token2.name, @"name"); - XCTAssertEqualObjects(token2.issuer, @"issuér !"); - - // If there is an issuer argument different from the name prefix, - // trust the argument and leave the name as it is - OTPToken *token3 = [OTPToken tokenWithURL:[NSURL URLWithString:[urlString stringByAppendingString:@"&issuer=test"]]]; - - XCTAssertNotNil(token3, @"<%@> did not create a valid token.", urlString); - XCTAssertNotEqualObjects(token3.name, @"name"); - XCTAssertTrue([token3.name rangeOfString:@"issuér !"].location == 0, @"The name should begin with \"issuér !\""); - XCTAssertTrue([token3.name rangeOfString:@"name"].location == token3.name.length-4, @"The name should end with \"name\""); - XCTAssertEqualObjects(token3.issuer, @"test"); - } -} - - -#pragma mark Serialization - -- (void)testTOTPURL -{ - OTPToken *token = [OTPToken tokenWithURL:[NSURL URLWithString:@"otpauth://totp/L%C3%A9on?algorithm=SHA256&digits=8&period=45&secret=AAAQEAYEAUDAOCAJBIFQYDIOB4"]]; - NSURL *url = token.url; - - XCTAssertEqualObjects(url.scheme, @"otpauth"); - XCTAssertEqualObjects(url.host, @"totp"); - XCTAssertEqualObjects([url.path substringFromIndex:1], @"Léon"); - - NSArray *expectedQueryItems = @[[NSURLQueryItem queryItemWithName:@"algorithm" value:@"SHA256"], - [NSURLQueryItem queryItemWithName:@"digits" value:@"8"], - [NSURLQueryItem queryItemWithName:@"issuer" value:@""], - [NSURLQueryItem queryItemWithName:@"period" value:@"45"]]; - NSArray *queryItems = [NSURLComponents componentsWithURL:url - resolvingAgainstBaseURL:NO].queryItems; - XCTAssertEqualObjects(queryItems, expectedQueryItems); -} - -- (void)testHOTPURL -{ - OTPToken *token = [OTPToken tokenWithURL:[NSURL URLWithString:@"otpauth://hotp/L%C3%A9on?algorithm=SHA256&digits=8&counter=18446744073709551615&secret=AAAQEAYEAUDAOCAJBIFQYDIOB4"]]; - NSURL *url = token.url; - - XCTAssertEqualObjects(url.scheme, @"otpauth"); - XCTAssertEqualObjects(url.host, @"hotp"); - XCTAssertEqualObjects([url.path substringFromIndex:1], @"Léon"); - - NSArray *expectedQueryItems = @[[NSURLQueryItem queryItemWithName:@"algorithm" value:@"SHA256"], - [NSURLQueryItem queryItemWithName:@"digits" value:@"8"], - [NSURLQueryItem queryItemWithName:@"issuer" value:@""], - [NSURLQueryItem queryItemWithName:@"counter" value:@"18446744073709551615"]]; - NSArray *queryItems = [NSURLComponents componentsWithURL:url - resolvingAgainstBaseURL:NO].queryItems; - XCTAssertEqualObjects(queryItems, expectedQueryItems); -} - -@end diff --git a/OneTimePasswordLegacyTests/OTPTokenTests.swift b/OneTimePasswordLegacyTests/OTPTokenTests.swift deleted file mode 100644 index 542d48d..0000000 --- a/OneTimePasswordLegacyTests/OTPTokenTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// OTPTokenTests.swift -// OneTimePassword -// -// Copyright (c) 2015-2017 Matt Rubin and the OneTimePassword authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import XCTest - -class OTPTokenTests: XCTestCase { - func testInit() { - let token = OTPToken() - - XCTAssertEqual(token.name, "") - XCTAssertEqual(token.issuer, "") - XCTAssertEqual(token.type, OTPTokenType.timer) - XCTAssertEqual(token.secret, Data()) - XCTAssertEqual(token.algorithm, OTPAlgorithm.sha1) - XCTAssertEqual(token.digits, 6) - XCTAssertEqual(token.period, 30) - XCTAssertEqual(token.counter, 0) - } -} diff --git a/OneTimePasswordLegacyTests/OTPTypeStrings.h b/OneTimePasswordLegacyTests/OTPTypeStrings.h deleted file mode 100644 index 847b904..0000000 --- a/OneTimePasswordLegacyTests/OTPTypeStrings.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// OTPTypeStrings.h -// OneTimePassword -// -// Copyright (c) 2014-2015 Matt Rubin and the OneTimePassword authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@import Foundation; -#import "OneTimePasswordLegacyTests-Swift.h" - - -#pragma mark - OTPTokenType - -@interface NSString (OTPTokenType) -+ (instancetype)stringForTokenType:(OTPTokenType)tokenType; -@end - - -#pragma mark - OTPAlgorithm - -extern NSString *const kOTPAlgorithmSHA1; -extern NSString *const kOTPAlgorithmSHA256; -extern NSString *const kOTPAlgorithmSHA512; - -@interface NSString (OTPAlgorithm) -+ (instancetype)stringForAlgorithm:(OTPAlgorithm)algorithm; -@end diff --git a/OneTimePasswordLegacyTests/OTPTypeStrings.m b/OneTimePasswordLegacyTests/OTPTypeStrings.m deleted file mode 100644 index 5eb3ec3..0000000 --- a/OneTimePasswordLegacyTests/OTPTypeStrings.m +++ /dev/null @@ -1,69 +0,0 @@ -// -// OTPTypeStrings.m -// OneTimePassword -// -// Copyright (c) 2014-2019 Matt Rubin and the OneTimePassword authors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -#import "OTPTypeStrings.h" - - -#pragma mark - OTPTokenType - -NSString *const kOTPTokenTypeCounter = @"hotp"; -NSString *const kOTPTokenTypeTimer = @"totp"; - -@implementation NSString (OTPTokenType) - -+ (instancetype)stringForTokenType:(OTPTokenType)tokenType -{ - switch (tokenType) { - case OTPTokenTypeCounter: - return kOTPTokenTypeCounter; - case OTPTokenTypeTimer: - return kOTPTokenTypeTimer; - } -} - -@end - - -#pragma mark - OTPAlgorithm - -NSString *const kOTPAlgorithmSHA1 = @"SHA1"; -NSString *const kOTPAlgorithmSHA256 = @"SHA256"; -NSString *const kOTPAlgorithmSHA512 = @"SHA512"; - -@implementation NSString (OTPAlgorithm) - -+ (instancetype)stringForAlgorithm:(OTPAlgorithm)algorithm -{ - switch (algorithm) { - case OTPAlgorithmSHA1: - return kOTPAlgorithmSHA1; - case OTPAlgorithmSHA256: - return kOTPAlgorithmSHA256; - case OTPAlgorithmSHA512: - return kOTPAlgorithmSHA512; - } -} - -@end diff --git a/Tests/TokenSerializationTests.swift b/Tests/TokenSerializationTests.swift index ef3e541..f2f8f69 100644 --- a/Tests/TokenSerializationTests.swift +++ b/Tests/TokenSerializationTests.swift @@ -2,7 +2,7 @@ // TokenSerializationTests.swift // OneTimePassword // -// Copyright (c) 2014-2018 Matt Rubin and the OneTimePassword authors +// Copyright (c) 2014-2022 Matt Rubin and the OneTimePassword authors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -23,18 +23,27 @@ // SOFTWARE. // -import XCTest +import Base32 import OneTimePassword +import XCTest + +private let validSecret: [UInt8] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, +] +// swiftlint:disable:next type_body_length class TokenSerializationTests: XCTestCase { let kOTPScheme = "otpauth" let kOTPTokenTypeCounterHost = "hotp" - let kOTPTokenTypeTimerHost = "totp" + let kOTPTokenTypeTimerHost = "totp" + let kOTPAlgorithmSHA1 = "SHA1" + let kOTPAlgorithmSHA256 = "SHA256" + let kOTPAlgorithmSHA512 = "SHA512" - let factors: [OneTimePassword.Generator.Factor] = [ + let factors: [Generator.Factor] = [ .counter(0), .counter(1), - .counter(UInt64.max), + .counter(.max), .timer(period: 1), .timer(period: 30), .timer(period: 300), @@ -47,9 +56,134 @@ class TokenSerializationTests: XCTestCase { "1234567890123456789012345678901234567890123456789012345678901234", "", ] - let algorithms: [OneTimePassword.Generator.Algorithm] = [.sha1, .sha256, .sha512] + let algorithms: [Generator.Algorithm] = [.sha1, .sha256, .sha512] let digits = [6, 7, 8] + // MARK: mark - Brute Force Tests + + func testDeserialization() throws { + for factor in factors { + for name in names { + for issuer in issuers { + for secretString in secretStrings { + for algorithm in algorithms { + for digitNumber in digits { + let secret = secretString.data(using: .ascii)! + + // Construct the URL + var urlComponents = URLComponents() + urlComponents.scheme = kOTPScheme + urlComponents.host = urlHost(for: factor) + urlComponents.path = "/" + name + + var queryItems: [URLQueryItem] = [] + let algorithmValue = string(for: algorithm) + queryItems.append(URLQueryItem(name: "algorithm", value: algorithmValue)) + queryItems.append(URLQueryItem(name: "digits", value: String(digitNumber))) + let secretValue = MF_Base32Codec.base32String(from: secret) + .replacingOccurrences(of: "=", with: "") + queryItems.append(URLQueryItem(name: "secret", value: secretValue)) + switch factor { + case .timer(let period): + let periodValue = String(Int(period)) + queryItems.append(URLQueryItem(name: "period", value: periodValue)) + + case .counter(let count): + let counterValue = String(count) + queryItems.append(URLQueryItem(name: "counter", value: counterValue)) + } + queryItems.append(URLQueryItem(name: "issuer", value: issuer)) + urlComponents.queryItems = queryItems + let url = urlComponents.url! + + // Create the token + let token = try Token(url: url) + + XCTAssertEqual(token.generator.factor, factor, "Incorrect token type") + XCTAssertEqual(token.name, name, "Incorrect token name") + XCTAssertEqual(token.issuer, issuer, "Incorrect token issuer") + XCTAssertEqual(token.generator.secret, secret, "Incorrect token secret") + XCTAssertEqual(token.generator.algorithm, algorithm, "Incorrect token algorithm") + XCTAssertEqual(token.generator.digits, digitNumber, "Incorrect token digits") + } + } + } + } + } + } + } + + private func urlHost(for factor: Generator.Factor) -> String { + switch factor { + case .counter: + return kOTPTokenTypeCounterHost + case .timer: + return kOTPTokenTypeTimerHost + } + } + + private func string(for algorithm: Generator.Algorithm) -> String { + switch algorithm { + case .sha1: + return kOTPAlgorithmSHA1 + case .sha256: + return kOTPAlgorithmSHA256 + case .sha512: + return kOTPAlgorithmSHA512 + } + } + + func testTokenWithURLAndSecret() throws { + for factor in factors { + for name in names { + for issuer in issuers { + for secretString in secretStrings { + for algorithm in algorithms { + for digitNumber in digits { + let secret = secretString.data(using: .ascii)! + + // Construct the URL + var urlComponents = URLComponents() + urlComponents.scheme = kOTPScheme + urlComponents.host = urlHost(for: factor) + urlComponents.path = "/" + name + + var queryItems: [URLQueryItem] = [] + let algorithmValue = string(for: algorithm) + queryItems.append(URLQueryItem(name: "algorithm", value: algorithmValue)) + queryItems.append(URLQueryItem(name: "digits", value: String(digitNumber))) + // TODO: Test secret overriding in a separate test case + queryItems.append(URLQueryItem(name: "secret", value: "A")) + switch factor { + case .timer(let period): + let periodValue = String(Int(period)) + queryItems.append(URLQueryItem(name: "period", value: periodValue)) + + case .counter(let count): + let counterValue = String(count) + queryItems.append(URLQueryItem(name: "counter", value: counterValue)) + } + queryItems.append(URLQueryItem(name: "issuer", value: issuer)) + urlComponents.queryItems = queryItems + let url = urlComponents.url! + + // Create the token + let token = try Token(url: url, secret: secret) + + XCTAssertEqual(token.generator.factor, factor, "Incorrect token type") + XCTAssertEqual(token.name, name, "Incorrect token name") + XCTAssertEqual(token.issuer, issuer, "Incorrect token issuer") + XCTAssertEqual(token.generator.secret, secret, "Incorrect token secret") + XCTAssertEqual(token.generator.algorithm, algorithm, "Incorrect token algorithm") + XCTAssertEqual(token.generator.digits, digitNumber, "Incorrect token digits") + } + } + } + } + } + } + } + // swiftlint:disable:next function_body_length func testSerialization() throws { for factor in factors { @@ -61,11 +195,10 @@ class TokenSerializationTests: XCTestCase { // Create the token let generator = try Generator( factor: factor, - secret: secretString.data(using: String.Encoding.ascii)!, + secret: secretString.data(using: .ascii)!, algorithm: algorithm, digits: digitNumber ) - let token = Token( name: name, issuer: issuer, @@ -78,45 +211,34 @@ class TokenSerializationTests: XCTestCase { // Test scheme XCTAssertEqual(url.scheme, kOTPScheme, "The url scheme should be \"\(kOTPScheme)\"") // Test Factor - var expectedHost: String - switch factor { - case .counter: - expectedHost = kOTPTokenTypeCounterHost - case .timer: - expectedHost = kOTPTokenTypeTimerHost - } - XCTAssertEqual(url.host!, expectedHost, "The url host should be \"\(expectedHost)\"") + let expectedHost = urlHost(for: factor) + XCTAssertEqual(url.host, expectedHost, "The url host should be \"\(expectedHost)\"") // Test name XCTAssertEqual(url.path, "/" + name, "The url path should be \"/\(name)\"") let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) - let items = urlComponents?.queryItems + let queryItems = urlComponents?.queryItems ?? [] let expectedItemCount = 4 - XCTAssertEqual(items?.count, expectedItemCount, + XCTAssertEqual(queryItems.count, expectedItemCount, "There shouldn't be any unexpected query arguments: \(url)") var queryArguments: [String: String] = [:] - for item in items ?? [] { - queryArguments[item.name] = item.value + for queryItem in queryItems { + XCTAssertNil(queryArguments[queryItem.name]) + queryArguments[queryItem.name] = queryItem.value } XCTAssertEqual(queryArguments.count, expectedItemCount, "There shouldn't be any unexpected query arguments: \(url)") // Test algorithm - let algorithmString: String = { - switch $0 { - case .sha1: - return "SHA1" - case .sha256: - return "SHA256" - case .sha512: - return "SHA512" - }}(algorithm) - XCTAssertEqual(queryArguments["algorithm"]!, algorithmString, - "The algorithm value should be \"\(algorithmString)\"") + let expectedAlgorithmString = string(for: algorithm) + XCTAssertEqual(queryArguments["algorithm"], expectedAlgorithmString, + "The algorithm value should be \"\(expectedAlgorithmString)\"") + // Test digits - XCTAssertEqual(queryArguments["digits"]!, String(digitNumber), - "The digits value should be \"\(digitNumber)\"") + let expectedDigitsString = String(digitNumber) + XCTAssertEqual(queryArguments["digits"], expectedDigitsString, + "The digits value should be \"\(expectedDigitsString)\"") // Test secret XCTAssertNil(queryArguments["secret"], "The url query string should not contain the secret") @@ -124,24 +246,28 @@ class TokenSerializationTests: XCTestCase { // Test period switch factor { case .timer(let period): - XCTAssertEqual(queryArguments["period"]!, String(Int(period)), - "The period value should be \"\(period)\"") + let expectedPeriodString = String(Int(period)) + XCTAssertEqual(queryArguments["period"], expectedPeriodString, + "The period value should be \"\(expectedPeriodString)\"") + default: XCTAssertNil(queryArguments["period"], "The url query string should not contain the period") } // Test counter switch factor { - case .counter(let counter): - XCTAssertEqual(queryArguments["counter"]!, String(counter), - "The counter value should be \"\(counter)\"") + case .counter(let count): + let expectedCounterString = String(count) + XCTAssertEqual(queryArguments["counter"], expectedCounterString, + "The counter value should be \"\(expectedCounterString)\"") + default: XCTAssertNil(queryArguments["counter"], "The url query string should not contain the counter") } // Test issuer - XCTAssertEqual(queryArguments["issuer"]!, issuer, + XCTAssertEqual(queryArguments["issuer"], issuer, "The issuer value should be \"\(issuer)\"") // Check url again @@ -164,4 +290,152 @@ class TokenSerializationTests: XCTestCase { let token = try Token(url: tokenURL) XCTAssertEqual(token.generator.factor, .counter(0)) } + + // MARK: - Test with specific URLs + // From Google Authenticator for iOS + // https://code.google.com/p/google-authenticator/source/browse/mobile/ios/Classes/OTPAuthURLTest.m + + // MARK: Deserialization + + func testTokenWithTOTPURL() throws { + let urlString = "otpauth://totp/L%C3%A9on?algorithm=SHA256&digits=8&period=45&secret=AAAQEAYEAUDAOCAJBIFQYDIOB4" + let token = try Token(url: URL(string: urlString)!) + + XCTAssertEqual(token.name, "Léon") + XCTAssertEqual(token.generator.secret, Data(bytes: validSecret, count: validSecret.count)) + XCTAssertEqual(token.generator.factor, Generator.Factor.timer(period: 45)) + XCTAssertEqual(token.generator.algorithm, Generator.Algorithm.sha256) + XCTAssertEqual(token.generator.digits, 8) + } + + func testTokenWithHOTPURL() throws { + let urlString = "otpauth://hotp/L%C3%A9on?algorithm=SHA256&digits=8&counter=18446744073709551615" + + "&secret=AAAQEAYEAUDAOCAJBIFQYDIOB4" + let secret = Data(bytes: validSecret, count: validSecret.count) + let token = try Token(url: URL(string: urlString)!) + + XCTAssertEqual(token.name, "Léon") + XCTAssertEqual(token.generator.secret, secret) + XCTAssertEqual(token.generator.factor, Generator.Factor.counter(18446744073709551615)) + XCTAssertEqual(token.generator.algorithm, Generator.Algorithm.sha256) + XCTAssertEqual(token.generator.digits, 8) + } + + func testTokenWithInvalidURLs() throws { + let badURLs = [ + "http://foo", // invalid scheme + "otpauth://foo", // invalid type + "otpauth:///bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4", // missing type + "otpauth://totp/bar", // missing secret + "otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&period=0", // invalid period + "otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&period=x", // non-numeric period + "otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&period=30&period=60", // multiple period + "otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&algorithm=MD5", // invalid algorithm + "otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&digits=2", // invalid digits + "otpauth://totp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&digits=x", // non-numeric digits + "otpauth://hotp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&counter=1.5", // invalid counter + "otpauth://hotp/bar?secret=AAAQEAYEAUDAOCAJBIFQYDIOB4&counter=x", // non-numeric counter + ] + + for badURL in badURLs { + let token = try? Token(url: URL(string: badURL)!) + XCTAssertNil(token, "Invalid url (\(badURL)) generated \(String(describing: token))") + } + } + + func testTokenWithIssuer() throws { + let simpleToken = try Token(url: URL(string: "otpauth://totp/name?secret=A&issuer=issuer")!) + XCTAssertNotNil(simpleToken) + XCTAssertEqual(simpleToken.name, "name") + XCTAssertEqual(simpleToken.issuer, "issuer") + + // TODO: test this more thoroughly, including the override case with + // "otpauth://totp/_issuer:name?secret=A&isser=issuer" + + let urlStrings = [ + "otpauth://totp/issu%C3%A9r%20!:name?secret=A", + "otpauth://totp/issu%C3%A9r%20!:%20name?secret=A", + "otpauth://totp/issu%C3%A9r%20!:%20%20%20name?secret=A", + "otpauth://totp/issu%C3%A9r%20!%3Aname?secret=A", + "otpauth://totp/issu%C3%A9r%20!%3A%20name?secret=A", + "otpauth://totp/issu%C3%A9r%20!%3A%20%20%20name?secret=A", + ] + for urlString in urlStrings { + // If there is no issuer argument, extract the issuer from the name + let token = try Token(url: URL(string: urlString)!) + + XCTAssertNotNil(token, "<\(urlString)> did not create a valid token.") + XCTAssertEqual(token.name, "name") + XCTAssertEqual(token.issuer, "issuér !") + + // If there is an issuer argument which matches the one in the name, trim the name + let token2 = try Token(url: URL(string: urlString.appending("&issuer=issu%C3%A9r%20!"))!) + + XCTAssertNotNil(token2, "<\(urlString)> did not create a valid token.") + XCTAssertEqual(token2.name, "name") + XCTAssertEqual(token2.issuer, "issuér !") + + // If there is an issuer argument different from the name prefix, + // trust the argument and leave the name as it is + let token3 = try Token(url: URL(string: urlString.appending("&issuer=test"))!) + + XCTAssertNotNil(token3, "<\(urlString)> did not create a valid token.") + XCTAssertNotEqual(token3.name, "name") + XCTAssertTrue(token3.name.hasPrefix("issuér !"), "The name should begin with \"issuér !\"") + XCTAssertTrue(token3.name.hasSuffix("name"), "The name should end with \"name\"") + XCTAssertEqual(token3.issuer, "test") + } + } + + // MARK: Serialization + + func testTOTPURL() throws { + let secret = MF_Base32Codec.data(fromBase32String: "AAAQEAYEAUDAOCAJBIFQYDIOB4")! + let generator = try Generator(factor: .timer(period: 45), secret: secret, algorithm: .sha256, digits: 8) + let token = Token(name: "Léon", generator: generator) + + // swiftlint:disable:next force_try + let url = try! token.toURL() + + XCTAssertEqual(url.scheme, "otpauth") + XCTAssertEqual(url.host, "totp") + XCTAssertEqual(url.path, "/Léon") + + let expectedQueryItems = [ + URLQueryItem(name: "algorithm", value: "SHA256"), + URLQueryItem(name: "digits", value: "8"), + URLQueryItem(name: "issuer", value: ""), + URLQueryItem(name: "period", value: "45"), + ] + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + XCTAssertEqual(queryItems, expectedQueryItems) + } + + func testHOTPURL() throws { + let secret = MF_Base32Codec.data(fromBase32String: "AAAQEAYEAUDAOCAJBIFQYDIOB4")! + let generator = try Generator( + factor: .counter(18446744073709551615), + secret: secret, + algorithm: .sha256, + digits: 8) + let token = Token(name: "Léon", generator: generator) + + // swiftlint:disable:next force_try + let url = try! token.toURL() + + XCTAssertEqual(url.scheme, "otpauth") + XCTAssertEqual(url.host, "hotp") + XCTAssertEqual(url.path, "/Léon") + + let expectedQueryItems = [ + URLQueryItem(name: "algorithm", value: "SHA256"), + URLQueryItem(name: "digits", value: "8"), + URLQueryItem(name: "issuer", value: ""), + URLQueryItem(name: "counter", value: "18446744073709551615"), + ] + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + XCTAssertEqual(queryItems, expectedQueryItems) + } } + +// swiftlint:disable:this file_length