diff --git a/SwiftOTP.xcodeproj/project.pbxproj b/SwiftOTP.xcodeproj/project.pbxproj index 4976f2e..8571331 100644 --- a/SwiftOTP.xcodeproj/project.pbxproj +++ b/SwiftOTP.xcodeproj/project.pbxproj @@ -44,6 +44,10 @@ 0E5B38DB200B1B4D00A419CA /* HOTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5B38DA200B1B4D00A419CA /* HOTP.swift */; }; 0E5B38DD200B1D3700A419CA /* HOTPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5B38DC200B1D3700A419CA /* HOTPTests.swift */; }; 0E5B38DF200B1D4600A419CA /* TOTPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5B38DE200B1D4600A419CA /* TOTPTests.swift */; }; + C19F90C52B738C1700E4E7DA /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19F90C42B738C1700E4E7DA /* Error.swift */; }; + C19F90C62B738C1700E4E7DA /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19F90C42B738C1700E4E7DA /* Error.swift */; }; + C19F90C72B738C1700E4E7DA /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19F90C42B738C1700E4E7DA /* Error.swift */; }; + C19F90C82B738C1700E4E7DA /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19F90C42B738C1700E4E7DA /* Error.swift */; }; FC338DB625460E4300F5871E /* UInt64+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E303A7621A3A742002FB8E0 /* UInt64+Data.swift */; }; FC338DB725460E4400F5871E /* UInt64+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E303A7621A3A742002FB8E0 /* UInt64+Data.swift */; }; FC338DB825460E4500F5871E /* UInt64+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E303A7621A3A742002FB8E0 /* UInt64+Data.swift */; }; @@ -85,6 +89,7 @@ 0E5B38DA200B1B4D00A419CA /* HOTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HOTP.swift; sourceTree = ""; }; 0E5B38DC200B1D3700A419CA /* HOTPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HOTPTests.swift; sourceTree = ""; }; 0E5B38DE200B1D4600A419CA /* TOTPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPTests.swift; sourceTree = ""; }; + C19F90C42B738C1700E4E7DA /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; FC338DB025460B7300F5871E /* CryptoSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoSwift.framework; path = Carthage/Build/iOS/CryptoSwift.framework; sourceTree = ""; }; FC338DB225460DD000F5871E /* CryptoSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoSwift.framework; path = Carthage/Build/Mac/CryptoSwift.framework; sourceTree = ""; }; FC338DB425460DFB00F5871E /* CryptoSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoSwift.framework; path = Carthage/Build/watchOS/CryptoSwift.framework; sourceTree = ""; }; @@ -188,6 +193,7 @@ isa = PBXGroup; children = ( 0E5B38D5200ACD2000A419CA /* Base32 */, + C19F90C42B738C1700E4E7DA /* Error.swift */, 0E5B38A02008BCB200A419CA /* OTPAlgorithm.swift */, 0E5B38DA200B1B4D00A419CA /* HOTP.swift */, 0E5B38A12008BCB200A419CA /* TOTP.swift */, @@ -464,6 +470,7 @@ 0E45381B2157766000B417E4 /* TOTP.swift in Sources */, 0E45381D2157766500B417E4 /* Base32.swift in Sources */, 0E4538192157766000B417E4 /* OTPAlgorithm.swift in Sources */, + C19F90C62B738C1700E4E7DA /* Error.swift in Sources */, 0E2A126825D15D670014486C /* Data+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -479,6 +486,7 @@ 0E45383E2157773800B417E4 /* Generator.swift in Sources */, 0E4538392157773800B417E4 /* Base32.swift in Sources */, 0E45383D2157773800B417E4 /* TOTP.swift in Sources */, + C19F90C72B738C1700E4E7DA /* Error.swift in Sources */, 0E2A126925D15D670014486C /* Data+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -494,6 +502,7 @@ 0E4538442157773800B417E4 /* Generator.swift in Sources */, 0E45383F2157773800B417E4 /* Base32.swift in Sources */, 0E4538432157773800B417E4 /* TOTP.swift in Sources */, + C19F90C82B738C1700E4E7DA /* Error.swift in Sources */, 0E2A126A25D15D680014486C /* Data+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -509,6 +518,7 @@ 0E5B38D9200ACDBC00A419CA /* Base32.swift in Sources */, 0E5B38DB200B1B4D00A419CA /* HOTP.swift in Sources */, 0E5B38A32008BCB200A419CA /* OTPAlgorithm.swift in Sources */, + C19F90C52B738C1700E4E7DA /* Error.swift in Sources */, 0E2A126225D15D670014486C /* Data+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SwiftOTP/Error.swift b/SwiftOTP/Error.swift new file mode 100644 index 0000000..74d2fc6 --- /dev/null +++ b/SwiftOTP/Error.swift @@ -0,0 +1,50 @@ +// +// Error.swift +// SwiftOTP +// +// Created by Akivili Collindort on 2024/2/7. +// Copyright © 2024 Lachlan Bell. All rights reserved. +// +// 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. +// +// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish, +// distribute, sublicense, create a derivative work, and/or sell copies of the +// Software in any work that is designed, intended, or marketed for pedagogical or +// instructional purposes related to programming, coding, application development, +// or information technology. Permission for such use, copying, modification, +// merger, publication, distribution, sublicensing, creation of derivative works, +// or sale is expressly withheld. +// +// 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 + +enum OTPError: Error { + case incorrectDigitsLength(length: Int) + case invalidTimeIntervalSince1970 +} + +extension OTPError: CustomStringConvertible { + var description: String { + switch self { + case .incorrectDigitsLength(let length): + "Length of digit should be between 6 and 8, not \(length)." + case .invalidTimeIntervalSince1970: + "Time interval since 1970 must be positive." + } + } +} diff --git a/SwiftOTP/Generator.swift b/SwiftOTP/Generator.swift index def7b98..5aece7d 100644 --- a/SwiftOTP/Generator.swift +++ b/SwiftOTP/Generator.swift @@ -52,7 +52,7 @@ internal class Generator { /// - parameter counter: UInt64 Counter value /// - parameter digits: Number of digits for generated string in range 6...8, defaults to 6 /// - returns: One time password string, nil if error - func generateOTP(secret: Data, algorithm: OTPAlgorithm = .sha1, counter: UInt64, digits: Int = 6) -> String? { + func generateOTP(secret: Data, algorithm: OTPAlgorithm = .sha1, counter: UInt64, digits: Int = 6) -> String { // HMAC message data from counter as big endian let counterMessage = counter.bigEndian.data diff --git a/SwiftOTP/HOTP.swift b/SwiftOTP/HOTP.swift index e2f0764..70b7104 100644 --- a/SwiftOTP/HOTP.swift +++ b/SwiftOTP/HOTP.swift @@ -44,27 +44,29 @@ public struct HOTP { /// - parameter digits: Number of digits for generated string in range 6...8, defaults to 6 /// - parameter algorithm: The hashing algorithm to use of type OTPAlgorithm, defaults to SHA-1 /// - precondition: digits *must* be between 6 and 8 inclusive - public init?(secret: Data, digits: Int = 6, algorithm: OTPAlgorithm = .sha1) { + public init(secret: Data, digits: Int = 6, algorithm: OTPAlgorithm = .sha1) throws { self.secret = secret self.digits = digits self.algorithm = algorithm - guard validateDigits(digit: digits) else { return nil } + try validateDigits(digit: digits) } /// Generate one time password string from counter value /// - parameter counter: UInt64 counter value /// - returns: One time password string, nil if error /// - precondition: Counter value must be of type UInt64 - public func generate(counter: UInt64) -> String? { + public func generate(counter: UInt64) -> String { return Generator.shared.generateOTP(secret: secret, algorithm: algorithm, counter: counter, digits: digits) } /// Verify time integer is postive /// - parameter time: Time since Unix epoch (01 Jan 1970 00:00 UTC) /// - returns: Whether time is valid - private func validateDigits(digit: Int) -> Bool{ + private func validateDigits(digit: Int) throws { let validDigits = 6...8 - return validDigits.contains(digit) + guard validDigits.contains(digit) else { + throw OTPError.incorrectDigitsLength(length: digit) + } } } diff --git a/SwiftOTP/TOTP.swift b/SwiftOTP/TOTP.swift index d232fcc..a709e13 100644 --- a/SwiftOTP/TOTP.swift +++ b/SwiftOTP/TOTP.swift @@ -45,45 +45,48 @@ public struct TOTP { /// - parameter digits: Number of digits for generated string in range 6...8, defaults to 6 /// - parameter algorithm: The hashing algorithm to use of type OTPAlgorithm, defaults to SHA-1 /// - precondition: digits *must* be between 6 and 8 inclusive - public init?(secret: Data, digits: Int = 6, timeInterval: Int = 30, algorithm: OTPAlgorithm = .sha1) { + public init(secret: Data, digits: Int = 6, timeInterval: Int = 30, algorithm: OTPAlgorithm = .sha1) throws { self.secret = secret self.digits = digits self.timeInterval = timeInterval self.algorithm = algorithm - guard validateDigits(digit: digits) else { return nil } + try validateDigits(digit: digits) } /// Generate one time password string from Date object /// - parameter time: Date object to generate password for /// - returns: One time password string, nil if error - public func generate(time: Date) -> String? { + public func generate(time: Date) throws -> String { let secondsPast1970 = Int(floor(time.timeIntervalSince1970)) - return generate(secondsPast1970: secondsPast1970) + return try generate(secondsPast1970: secondsPast1970) } /// Generate one time password string from Unix time /// - parameter secondsPast1970: Time since Unix epoch (01 Jan 1970 00:00 UTC) /// - returns: One time password string, nil if error /// - precondition: secondsPast1970 must be a positive integer - public func generate(secondsPast1970: Int) -> String? { - guard validateTime(time: secondsPast1970) else { return nil } + public func generate(secondsPast1970: Int) throws -> String { + try validateTime(time: secondsPast1970) let counterValue = Int(floor(Double(secondsPast1970) / Double(timeInterval))) return Generator.shared.generateOTP(secret: secret, algorithm: algorithm, counter: UInt64(counterValue), digits: digits) } /// Check to see if digits value provided is in the range 6...8 (specified in RFC 4226) /// - parameter digit: Number of digits for generated string - private func validateDigits(digit: Int) -> Bool{ + private func validateDigits(digit: Int) throws { let validDigits = 6...8 - return validDigits.contains(digit) + guard validDigits.contains(digit) else { + throw OTPError.incorrectDigitsLength(length: digit) + } } /// Verify time integer is postive /// - parameter time: Time since Unix epoch (01 Jan 1970 00:00 UTC) /// - returns: Whether time is valid - private func validateTime(time: Int) -> Bool { - return (time > 0) + private func validateTime(time: Int) throws { + guard (time > 0) else { + throw OTPError.invalidTimeIntervalSince1970 + } } - } diff --git a/SwiftOTPTests/HOTPTests.swift b/SwiftOTPTests/HOTPTests.swift index e68e8ed..883e4b1 100644 --- a/SwiftOTPTests/HOTPTests.swift +++ b/SwiftOTPTests/HOTPTests.swift @@ -44,8 +44,8 @@ class HOTPTests: XCTestCase { let data = Data(hex: "3132333435363738393031323334353637383930") let expectedOTP = ["755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871", "520489"] - func testHOTP() { - let hotp = HOTP(secret: data)! + func testHOTP() throws { + let hotp = try HOTP(secret: data) for i in 0...(expectedOTP.count - 1) { XCTAssertEqual(hotp.generate(counter: UInt64(i)), expectedOTP[i]) } diff --git a/SwiftOTPTests/TOTPTests.swift b/SwiftOTPTests/TOTPTests.swift index f41726d..abe93bb 100644 --- a/SwiftOTPTests/TOTPTests.swift +++ b/SwiftOTPTests/TOTPTests.swift @@ -46,93 +46,93 @@ class TOTPTests: XCTestCase { let dataSHA256 = "12345678901234567890123456789012".data(using: String.Encoding.ascii)! let dataSHA512 = "1234567890123456789012345678901234567890123456789012345678901234".data(using: String.Encoding.ascii)! - func test01() { - let totp = TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) - XCTAssertEqual(totp?.generate(secondsPast1970: 59), "94287082") + func test01() throws { + let totp = try TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) + XCTAssertEqual(try totp.generate(secondsPast1970: 59), "94287082") } - func test02() { - let totp = TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) - XCTAssertEqual(totp?.generate(secondsPast1970: 59), "46119246") + func test02() throws { + let totp = try TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) + XCTAssertEqual(try totp.generate(secondsPast1970: 59), "46119246") } - func test03() { - let totp = TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) - XCTAssertEqual(totp?.generate(secondsPast1970: 59), "90693936") + func test03() throws { + let totp = try TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) + XCTAssertEqual(try totp.generate(secondsPast1970: 59), "90693936") } - func test04() { - let totp = TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) - XCTAssertEqual(totp?.generate(secondsPast1970: 1111111109), "07081804") + func test04() throws { + let totp = try TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) + XCTAssertEqual(try totp.generate(secondsPast1970: 1111111109), "07081804") } - func test05() { - let totp = TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) - XCTAssertEqual(totp?.generate(secondsPast1970: 1111111109), "68084774") + func test05() throws { + let totp = try TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) + XCTAssertEqual(try totp.generate(secondsPast1970: 1111111109), "68084774") } - func test06() { - let totp = TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) - XCTAssertEqual(totp?.generate(secondsPast1970: 1111111109), "25091201") + func test06() throws { + let totp = try TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) + XCTAssertEqual(try totp.generate(secondsPast1970: 1111111109), "25091201") } - func test07() { - let totp = TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) - XCTAssertEqual(totp?.generate(secondsPast1970: 1111111111), "14050471") + func test07() throws { + let totp = try TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) + XCTAssertEqual(try totp.generate(secondsPast1970: 1111111111), "14050471") } - func test08() { - let totp = TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) - XCTAssertEqual(totp?.generate(secondsPast1970: 1111111111), "67062674") + func test08() throws { + let totp = try TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) + XCTAssertEqual(try totp.generate(secondsPast1970: 1111111111), "67062674") } - func test09() { - let totp = TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) - XCTAssertEqual(totp?.generate(secondsPast1970: 1111111111), "99943326") + func test09() throws { + let totp = try TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) + XCTAssertEqual(try totp.generate(secondsPast1970: 1111111111), "99943326") } - func test10() { - let totp = TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) - XCTAssertEqual(totp?.generate(secondsPast1970: 1234567890), "89005924") + func test10() throws { + let totp = try TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) + XCTAssertEqual(try totp.generate(secondsPast1970: 1234567890), "89005924") } - func test11() { - let totp = TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) - XCTAssertEqual(totp?.generate(secondsPast1970: 1234567890), "91819424") + func test11() throws { + let totp = try TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) + XCTAssertEqual(try totp.generate(secondsPast1970: 1234567890), "91819424") } - func test12() { - let totp = TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) - XCTAssertEqual(totp?.generate(secondsPast1970: 1234567890), "93441116") + func test12() throws { + let totp = try TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) + XCTAssertEqual(try totp.generate(secondsPast1970: 1234567890), "93441116") } - func test13() { - let totp = TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) - XCTAssertEqual(totp?.generate(secondsPast1970: 2000000000), "69279037") + func test13() throws { + let totp = try TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) + XCTAssertEqual(try totp.generate(secondsPast1970: 2000000000), "69279037") } - func test14() { - let totp = TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) - XCTAssertEqual(totp?.generate(secondsPast1970: 2000000000), "90698825") + func test14() throws { + let totp = try TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) + XCTAssertEqual(try totp.generate(secondsPast1970: 2000000000), "90698825") } - func test15() { - let totp = TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) - XCTAssertEqual(totp?.generate(secondsPast1970: 2000000000), "38618901") + func test15() throws { + let totp = try TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) + XCTAssertEqual(try totp.generate(secondsPast1970: 2000000000), "38618901") } - func test16() { - let totp = TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) - XCTAssertEqual(totp?.generate(secondsPast1970: 20000000000), "65353130") + func test16() throws { + let totp = try TOTP(secret: dataSHA1, digits: 8, timeInterval: 30, algorithm: .sha1) + XCTAssertEqual(try totp.generate(secondsPast1970: 20000000000), "65353130") } - func test17() { - let totp = TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) - XCTAssertEqual(totp?.generate(secondsPast1970: 20000000000), "77737706") + func test17() throws { + let totp = try TOTP(secret: dataSHA256, digits: 8, timeInterval: 30, algorithm: .sha256) + XCTAssertEqual(try totp.generate(secondsPast1970: 20000000000), "77737706") } - func test18() { - let totp = TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) - XCTAssertEqual(totp?.generate(secondsPast1970: 20000000000), "47863826") + func test18() throws { + let totp = try TOTP(secret: dataSHA512, digits: 8, timeInterval: 30, algorithm: .sha512) + XCTAssertEqual(try totp.generate(secondsPast1970: 20000000000), "47863826") } }