| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- // LoopFollow
- // TOTPGenerator.swift
- import CommonCrypto
- import Foundation
- enum TOTPGenerator {
- /// Generates a TOTP code from a base32 secret
- /// - Parameter secret: The base32 encoded secret
- /// - Returns: A 6-digit TOTP code as a string
- static func generateTOTP(secret: String) -> String {
- // Decode base32 secret
- let decodedSecret = base32Decode(secret)
- // Get current time in 30-second intervals
- let timeInterval = Int(Date().timeIntervalSince1970)
- let timeStep = 30
- let counter = timeInterval / timeStep
- // Convert counter to 8-byte big-endian data
- var counterData = Data()
- for i in 0 ..< 8 {
- counterData.append(UInt8((counter >> (56 - i * 8)) & 0xFF))
- }
- // Generate HMAC-SHA1
- let key = Data(decodedSecret)
- let hmac = generateHMACSHA1(key: key, data: counterData)
- // Get the last 4 bits of the HMAC
- let offset = Int(hmac.withUnsafeBytes { $0.last! } & 0x0F)
- // Extract 4 bytes starting at the offset
- let hmacData = Data(hmac)
- let codeBytes = hmacData.subdata(in: offset ..< (offset + 4))
- // Convert to integer and get last 6 digits
- let code = codeBytes.withUnsafeBytes { bytes in
- let value = bytes.load(as: UInt32.self).bigEndian
- return Int(value & 0x7FFF_FFFF) % 1_000_000
- }
- return String(format: "%06d", code)
- }
- /// Extracts OTP from various URL formats
- /// - Parameter urlString: The URL string to parse
- /// - Returns: The OTP code as a string, or nil if not found
- static func extractOTPFromURL(_ urlString: String) -> String? {
- guard let url = URL(string: urlString),
- let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
- else {
- return nil
- }
- // Check for TOTP format (otpauth://)
- if url.scheme == "otpauth" {
- if let secretItem = components.queryItems?.first(where: { $0.name == "secret" }),
- let secret = secretItem.value
- {
- return generateTOTP(secret: secret)
- }
- }
- // Check for regular OTP format
- if let otpItem = components.queryItems?.first(where: { $0.name == "otp" }) {
- return otpItem.value
- }
- return nil
- }
- /// Decodes a base32 string to bytes
- /// - Parameter string: The base32 encoded string
- /// - Returns: Array of decoded bytes
- private static func base32Decode(_ string: String) -> [UInt8] {
- let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
- var result: [UInt8] = []
- var buffer = 0
- var bitsLeft = 0
- for char in string.uppercased() {
- guard let index = alphabet.firstIndex(of: char) else { continue }
- let value = alphabet.distance(from: alphabet.startIndex, to: index)
- buffer = (buffer << 5) | value
- bitsLeft += 5
- while bitsLeft >= 8 {
- bitsLeft -= 8
- result.append(UInt8((buffer >> bitsLeft) & 0xFF))
- }
- }
- return result
- }
- /// Generates HMAC-SHA1 for the given key and data
- /// - Parameters:
- /// - key: The key to use for HMAC
- /// - data: The data to hash
- /// - Returns: The HMAC-SHA1 result as Data
- private static func generateHMACSHA1(key: Data, data: Data) -> Data {
- var hmac = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
- key.withUnsafeBytes { keyBytes in
- data.withUnsafeBytes { dataBytes in
- CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), keyBytes.baseAddress, key.count, dataBytes.baseAddress, data.count, &hmac)
- }
- }
- return Data(hmac)
- }
- }
|