52 lines
1.9 KiB
Swift
52 lines
1.9 KiB
Swift
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<Insecure.SHA1>.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
|
|
}
|
|
}
|