Files
Thijooree-iOS/Thijooree iOS/Security/Totp.swift
T

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
}
}