274 lines
11 KiB
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)")
|
|
}
|
|
}
|
|
}
|
|
}
|