Files
OneTimePassword/Tests/GeneratorTests.swift

274 lines
11 KiB
Swift

//
// GeneratorTests.swift
// 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 XCTest
import OneTimePassword
class GeneratorTests: XCTestCase {
func testInit() throws {
// Create a generator
let factor = OneTimePassword.Generator.Factor.counter(111)
let secret = "12345678901234567890".data(using: String.Encoding.ascii)!
let algorithm = Generator.Algorithm.sha256
let digits = 8
let generator = try Generator(
factor: factor,
secret: secret,
algorithm: algorithm,
digits: digits
)
XCTAssertEqual(generator.factor, factor)
XCTAssertEqual(generator.secret, secret)
XCTAssertEqual(generator.algorithm, algorithm)
XCTAssertEqual(generator.digits, digits)
// Create another generator
let otherFactor = OneTimePassword.Generator.Factor.timer(period: 123)
let otherSecret = "09876543210987654321".data(using: String.Encoding.ascii)!
let otherAlgorithm = Generator.Algorithm.sha512
let otherDigits = 7
let otherGenerator = try Generator(
factor: otherFactor,
secret: otherSecret,
algorithm: otherAlgorithm,
digits: otherDigits
)
XCTAssertEqual(otherGenerator.factor, otherFactor)
XCTAssertEqual(otherGenerator.secret, otherSecret)
XCTAssertEqual(otherGenerator.algorithm, otherAlgorithm)
XCTAssertEqual(otherGenerator.digits, otherDigits)
// Ensure the generators are different
XCTAssertNotEqual(generator.factor, otherGenerator.factor)
XCTAssertNotEqual(generator.secret, otherGenerator.secret)
XCTAssertNotEqual(generator.algorithm, otherGenerator.algorithm)
XCTAssertNotEqual(generator.digits, otherGenerator.digits)
}
func testCounter() throws {
// swiftlint:disable:next large_tuple
let factors: [(TimeInterval, TimeInterval, UInt64)] = [
// swiftlint:disable comma
(100, 30, 3),
(10000, 30, 333),
(1000000, 30, 33333),
(100000000, 60, 1666666),
(10000000000, 90, 111111111),
// swiftlint:enable comma
]
for (timeSinceEpoch, period, count) in factors {
let time = Date(timeIntervalSince1970: timeSinceEpoch)
let timer = Generator.Factor.timer(period: period)
let counter = Generator.Factor.counter(count)
let secret = "12345678901234567890".data(using: String.Encoding.ascii)!
let hotp = try Generator(factor: counter, secret: secret, algorithm: .sha1, digits: 6).password(at: time)
let totp = try Generator(factor: timer, secret: secret, algorithm: .sha1, digits: 6).password(at: time)
XCTAssertEqual(hotp, totp,
"TOTP with \(timer) should match HOTP with counter \(counter) at time \(time).")
}
}
func testValidation() {
let digitTests: [(Int, Bool)] = [
(-6, false),
(0, false),
(1, false),
(5, false),
(6, true),
(7, true),
(8, true),
(9, false),
(10, false),
]
let periodTests: [(TimeInterval, Bool)] = [
(-30, false),
(0, false),
(1, true),
(30, true),
(300, true),
(301, true),
]
for (digits, digitsAreValid) in digitTests {
let generator = try? Generator(
factor: .counter(0),
secret: Data(),
algorithm: .sha1,
digits: digits
)
// If the digits are invalid, password generation should throw an error
let generatorIsValid = digitsAreValid
if generatorIsValid {
XCTAssertNotNil(generator)
} else {
XCTAssertNil(generator)
}
for (period, periodIsValid) in periodTests {
let generator = try? Generator(
factor: .timer(period: period),
secret: Data(),
algorithm: .sha1,
digits: digits
)
// If the digits or period are invalid, password generation should throw an error
let generatorIsValid = digitsAreValid && periodIsValid
if generatorIsValid {
XCTAssertNotNil(generator)
} else {
XCTAssertNil(generator)
}
}
}
}
func testPasswordAtInvalidTime() throws {
let generator = try Generator(
factor: .timer(period: 30),
secret: Data(),
algorithm: .sha1,
digits: 6
)
let badTime = Date(timeIntervalSince1970: -100)
XCTAssertThrowsError(try generator.password(at: badTime)) { error in
guard case Generator.Error.invalidTime = error else {
XCTFail("password(at: \(badTime)) threw an unexpected type of error: \(error)")
return
}
}
}
func testPasswordWithInvalidPeriod() {
// It should not be possible to try to get a password from a generator with an invalid period, because the
// generator initializer should fail when given an invalid period.
XCTAssertThrowsError(
try Generator(factor: .timer(period: 0), secret: Data(), algorithm: .sha1, digits: 8)
) { error in
guard case Generator.Error.invalidPeriod = error else {
XCTFail("Generator.init threw an unexpected type of error: \(error)")
return
}
}
}
func testPasswordWithInvalidDigits() {
// It should not be possible to try to get a password from a generator with an invalid digit count, because the
// generator initializer should fail when given an invalid digit count.
XCTAssertThrowsError(
try Generator(factor: .timer(period: 30), secret: Data(), algorithm: .sha1, digits: 3)
) { error in
guard case Generator.Error.invalidDigits = error else {
XCTFail("Generator.init threw an unexpected type of error: \(error)")
return
}
}
}
// The values in this test are found in Appendix D of the HOTP RFC
// https://tools.ietf.org/html/rfc4226#appendix-D
func testHOTPRFCValues() throws {
let secret = "12345678901234567890".data(using: String.Encoding.ascii)!
let expectedValues: [UInt64: String] = [
0: "755224",
1: "287082",
2: "359152",
3: "969429",
4: "338314",
5: "254676",
6: "287922",
7: "162583",
8: "399871",
9: "520489",
]
for (counter, expectedPassword) in expectedValues {
let generator = try Generator(factor: .counter(counter), secret: secret, algorithm: .sha1, digits: 6)
let time = Date(timeIntervalSince1970: 0)
let password = try generator.password(at: time)
XCTAssertEqual(password, expectedPassword,
"The generator did not produce the expected OTP.")
}
}
// The values in this test are found in Appendix B of the TOTP RFC
// https://tools.ietf.org/html/rfc6238#appendix-B
func testTOTPRFCValues() throws {
let secretKeys: [Generator.Algorithm: String] = [
.sha1: "12345678901234567890",
.sha256: "12345678901234567890123456789012",
.sha512: "1234567890123456789012345678901234567890123456789012345678901234",
]
let timesSinceEpoch: [TimeInterval] = [59, 1111111109, 1111111111, 1234567890, 2000000000, 20000000000]
let expectedValues: [Generator.Algorithm: [String]] = [
.sha1: ["94287082", "07081804", "14050471", "89005924", "69279037", "65353130"],
.sha256: ["46119246", "68084774", "67062674", "91819424", "90698825", "77737706"],
.sha512: ["90693936", "25091201", "99943326", "93441116", "38618901", "47863826"],
]
for (algorithm, secretKey) in secretKeys {
let secret = secretKey.data(using: String.Encoding.ascii)!
let generator = try Generator(factor: .timer(period: 30), secret: secret, algorithm: algorithm, digits: 8)
for (timeSinceEpoch, expectedPassword) in zip(timesSinceEpoch, expectedValues[algorithm]!) {
let time = Date(timeIntervalSince1970: timeSinceEpoch)
let password = try generator.password(at: time)
XCTAssertEqual(password, expectedPassword,
"Incorrect result for \(algorithm) at \(timeSinceEpoch)")
}
}
}
// From Google Authenticator for iOS
// https://code.google.com/p/google-authenticator/source/browse/mobile/ios/Classes/TOTPGeneratorTest.m
func testTOTPGoogleValues() throws {
let secret = "12345678901234567890".data(using: String.Encoding.ascii)!
let timesSinceEpoch: [TimeInterval] = [1111111111, 1234567890, 2000000000]
let expectedValues: [Generator.Algorithm: [String]] = [
.sha1: ["050471", "005924", "279037"],
.sha256: ["584430", "829826", "428693"],
.sha512: ["380122", "671578", "464532"],
]
for (algorithm, expectedPasswords) in expectedValues {
let generator = try Generator(factor: .timer(period: 30), secret: secret, algorithm: algorithm, digits: 6)
for (timeSinceEpoch, expectedPassword) in zip(timesSinceEpoch, expectedPasswords) {
let time = Date(timeIntervalSince1970: timeSinceEpoch)
let password = try generator.password(at: time)
XCTAssertEqual(password, expectedPassword,
"Incorrect result for \(algorithm) at \(timeSinceEpoch)")
}
}
}
}