Forked from alvarhansen/gist:e05da5a9a757b826d06c839f688c27bc
Created
April 22, 2024 18:40
-
-
Save trungnguyen1791/5db7e94d39ec74868d2c3491ea60b636 to your computer and use it in GitHub Desktop.
CryptoSwift TOTP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import CryptoSwift | |
import Foundation | |
// Combined from https://github.com/lachlanbell/SwiftOTP/blob/master/SwiftOTP/Generator.swift and https://raw.githubusercontent.com/vapor/open-crypto/3.3.3/Sources/Crypto/MAC/OTP.swift | |
// MARK: TOTP | |
/// Generates Time-based One-time Passwords using HMAC. | |
/// | |
/// let code = TOTP.SHA1.generate(secret: "hi") | |
/// print(code) "123456" | |
/// | |
/// You can also generate ranges using `generateRange(...)`. | |
public struct TOTP { | |
public init() { | |
} | |
/// Generates a range of TOTP tokens to a specific degree. This method | |
/// calls the `generate(...)` method internally. | |
/// | |
/// let codes = try TOTP.SHA1.generateRange(degree: 1, secret: key) | |
/// print(codes) // [String] | |
/// | |
/// - parameters: | |
/// - degree: Number of codes (in addition to the main code) to generate in both the forward | |
/// and backward direction. This number must be at least 1. For each degree, the total | |
/// code count will be increased by two: one for an additional degree in the positive | |
/// and negative offset directions. | |
/// For example, if `degree` is `2`, there will be `5` codes returned: The main code, | |
/// two codes at offset 1 (1 and -1), and two codes at offset 2 (2 and -2). | |
/// - digits: Number of digits to include in the password. | |
/// Defaults to six. | |
/// - secret: Cleartext (_not_ Base32 encoded) secret key. | |
/// - date: Date to generate the TOTP for. This will be divided into intervals automatically. | |
public func generateRange(degree: Int, digits: OTPDigits = .six, secret: Data, at date: Date = .init()) throws -> [String] { | |
var res: [String] = try [ | |
generate(digits: digits, secret: secret, offset: 0, at: date) | |
] | |
for i in 1...degree { | |
try res.append(generate(digits: digits, secret: secret, offset: i, at: date)) | |
try res.append(generate(digits: digits, secret: secret, offset: -1 * i, at: date)) | |
} | |
return res | |
} | |
/// Generates a single TOTP. | |
/// | |
/// let code = TOTP.SHA1.generate(secret: "hi") | |
/// print(code) "123456" | |
/// | |
/// - parameters: | |
/// - digits: Number of digits to include in the password. | |
/// Defaults to six. | |
/// - secret: Cleartext (_not_ Base32 encoded) secret key. | |
/// - offset: Specific offset (in intervals) from the supplied date. | |
/// Defaults to 0. | |
/// - date: Date to generate the TOTP for. This will be divided into intervals automatically. | |
public func generate(digits: OTPDigits = .six, secret: Data, offset: Int = 0, at date: Date = .init()) throws -> String { | |
let counter = floor(floor(date.timeIntervalSince1970) / 30) | |
return try generateOTP(secret: secret, counter: UInt64(counter - Double(offset)), digits: digits) | |
} | |
} | |
// MARK: OTP | |
/// Supported OTP password length. | |
public enum OTPDigits: Int { | |
/// Six digit password. | |
case six = 6 | |
/// Seven digit password. | |
case seven = 7 | |
/// Eight digit password. | |
case eight = 8 | |
/// Returns 10^digit | |
internal var pow: UInt32 { | |
switch self { | |
case .six: return 1_000_000 | |
case .seven: return 10_000_000 | |
case .eight: return 100_000_000 | |
} | |
} | |
} | |
// MARK: Private | |
private func generateOTP(secret: Data, counter: UInt64, digits: OTPDigits) throws -> String { | |
let digest = try HMAC(key: secret.bytes, variant: .sha256).authenticate(counter.bigEndian.data.bytes) | |
// get last 4 bits of hash for use as offset | |
let offset = Int(digest[digest.count - 1] & 0x0f) | |
// get 4 bytes of the hash using offset | |
let subdigest = Data(digest[offset...offset + 3]) | |
// convert data to UInt32 | |
var num = subdigest.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt32.self).pointee.bigEndian } | |
// remove most sig bit | |
num &= 0x7fffffff | |
// modulo num by digits | |
num = num % digits.pow | |
// convert to readable num | |
let desc = num.description | |
return String(repeating: "0", count: digits.rawValue - desc.count) + desc | |
} | |
private extension FixedWidthInteger { | |
var data: Data { | |
var int = self | |
return .init(bytes: &int, count: MemoryLayout<Self>.size) | |
} | |
} | |
let secret: Data = "SomeData".data(using: .utf8)! | |
let time = Date(timeIntervalSince1970: 1565609692) | |
let totp = try! TOTP().generate(digits: .six, secret: secret, at: time) | |
print(totp) | |
let range: [String]? = try? TOTP().generateRange(degree: 1, digits: .six, secret: secret, at: time) | |
print(range) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment