Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions SwiftOTP.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -85,6 +89,7 @@
0E5B38DA200B1B4D00A419CA /* HOTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HOTP.swift; sourceTree = "<group>"; };
0E5B38DC200B1D3700A419CA /* HOTPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HOTPTests.swift; sourceTree = "<group>"; };
0E5B38DE200B1D4600A419CA /* TOTPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPTests.swift; sourceTree = "<group>"; };
C19F90C42B738C1700E4E7DA /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
FC338DB025460B7300F5871E /* CryptoSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoSwift.framework; path = Carthage/Build/iOS/CryptoSwift.framework; sourceTree = "<group>"; };
FC338DB225460DD000F5871E /* CryptoSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoSwift.framework; path = Carthage/Build/Mac/CryptoSwift.framework; sourceTree = "<group>"; };
FC338DB425460DFB00F5871E /* CryptoSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoSwift.framework; path = Carthage/Build/watchOS/CryptoSwift.framework; sourceTree = "<group>"; };
Expand Down Expand Up @@ -188,6 +193,7 @@
isa = PBXGroup;
children = (
0E5B38D5200ACD2000A419CA /* Base32 */,
C19F90C42B738C1700E4E7DA /* Error.swift */,
0E5B38A02008BCB200A419CA /* OTPAlgorithm.swift */,
0E5B38DA200B1B4D00A419CA /* HOTP.swift */,
0E5B38A12008BCB200A419CA /* TOTP.swift */,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
50 changes: 50 additions & 0 deletions SwiftOTP/Error.swift
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
2 changes: 1 addition & 1 deletion SwiftOTP/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 7 additions & 5 deletions SwiftOTP/HOTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
25 changes: 14 additions & 11 deletions SwiftOTP/TOTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

}
4 changes: 2 additions & 2 deletions SwiftOTPTests/HOTPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down
108 changes: 54 additions & 54 deletions SwiftOTPTests/TOTPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}