import CryptoKit import Foundation enum Totp { // timeStep offsets the counter by N periods — use -1/+1 to handle clock skew at window boundaries. static func generate(_ base32Secret: String, digits: Int = 6, period: Int = 30, timeStep: Int = 0) -> String { let counter = Int64(Date().timeIntervalSince1970) / Int64(period) + Int64(timeStep) guard let keyBytes = base32Decode(base32Secret.uppercased()) else { return String(repeating: "0", count: digits) } let otp = hotp(key: keyBytes, counter: counter, digits: digits) return String(format: "%0\(digits)d", otp) } private static func hotp(key: [UInt8], counter: Int64, digits: Int) -> Int { var counterBigEndian = counter.bigEndian let counterData = withUnsafeBytes(of: &counterBigEndian) { Data($0) } let hmac = HMAC.authenticationCode( for: counterData, using: SymmetricKey(data: Data(key)) ) let hash = Array(hmac) let offset = Int(hash[hash.count - 1] & 0x0F) let truncated = (Int(hash[offset]) & 0x7F) << 24 | Int(hash[offset + 1]) << 16 | Int(hash[offset + 2]) << 8 | Int(hash[offset + 3]) let modulus = Int(pow(10.0, Double(digits))) return truncated % modulus } private static func base32Decode(_ input: String) -> [UInt8]? { let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" var bits = 0 var bitCount = 0 var result: [UInt8] = [] for char in input where char != "=" { guard let idx = alphabet.firstIndex(of: char) else { return nil } let value = alphabet.distance(from: alphabet.startIndex, to: idx) bits = (bits << 5) | value bitCount += 5 if bitCount >= 8 { bitCount -= 8 result.append(UInt8((bits >> bitCount) & 0xFF)) } } return result } }