TOTPGenerator.swift 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. // LoopFollow
  2. // TOTPGenerator.swift
  3. import CommonCrypto
  4. import Foundation
  5. enum TOTPGenerator {
  6. /// Generates a TOTP code from a base32 secret
  7. /// - Parameter secret: The base32 encoded secret
  8. /// - Returns: A 6-digit TOTP code as a string
  9. static func generateTOTP(secret: String) -> String {
  10. // Decode base32 secret
  11. let decodedSecret = base32Decode(secret)
  12. // Get current time in 30-second intervals
  13. let timeInterval = Int(Date().timeIntervalSince1970)
  14. let timeStep = 30
  15. let counter = timeInterval / timeStep
  16. // Convert counter to 8-byte big-endian data
  17. var counterData = Data()
  18. for i in 0 ..< 8 {
  19. counterData.append(UInt8((counter >> (56 - i * 8)) & 0xFF))
  20. }
  21. // Generate HMAC-SHA1
  22. let key = Data(decodedSecret)
  23. let hmac = generateHMACSHA1(key: key, data: counterData)
  24. // Get the last 4 bits of the HMAC
  25. let offset = Int(hmac.withUnsafeBytes { $0.last! } & 0x0F)
  26. // Extract 4 bytes starting at the offset
  27. let hmacData = Data(hmac)
  28. let codeBytes = hmacData.subdata(in: offset ..< (offset + 4))
  29. // Convert to integer and get last 6 digits
  30. let code = codeBytes.withUnsafeBytes { bytes in
  31. let value = bytes.load(as: UInt32.self).bigEndian
  32. return Int(value & 0x7FFF_FFFF) % 1_000_000
  33. }
  34. return String(format: "%06d", code)
  35. }
  36. /// Extracts OTP from various URL formats
  37. /// - Parameter urlString: The URL string to parse
  38. /// - Returns: The OTP code as a string, or nil if not found
  39. static func extractOTPFromURL(_ urlString: String) -> String? {
  40. guard let url = URL(string: urlString),
  41. let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
  42. else {
  43. return nil
  44. }
  45. // Check for TOTP format (otpauth://)
  46. if url.scheme == "otpauth" {
  47. if let secretItem = components.queryItems?.first(where: { $0.name == "secret" }),
  48. let secret = secretItem.value
  49. {
  50. return generateTOTP(secret: secret)
  51. }
  52. }
  53. // Check for regular OTP format
  54. if let otpItem = components.queryItems?.first(where: { $0.name == "otp" }) {
  55. return otpItem.value
  56. }
  57. return nil
  58. }
  59. /// Decodes a base32 string to bytes
  60. /// - Parameter string: The base32 encoded string
  61. /// - Returns: Array of decoded bytes
  62. private static func base32Decode(_ string: String) -> [UInt8] {
  63. let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
  64. var result: [UInt8] = []
  65. var buffer = 0
  66. var bitsLeft = 0
  67. for char in string.uppercased() {
  68. guard let index = alphabet.firstIndex(of: char) else { continue }
  69. let value = alphabet.distance(from: alphabet.startIndex, to: index)
  70. buffer = (buffer << 5) | value
  71. bitsLeft += 5
  72. while bitsLeft >= 8 {
  73. bitsLeft -= 8
  74. result.append(UInt8((buffer >> bitsLeft) & 0xFF))
  75. }
  76. }
  77. return result
  78. }
  79. /// Generates HMAC-SHA1 for the given key and data
  80. /// - Parameters:
  81. /// - key: The key to use for HMAC
  82. /// - data: The data to hash
  83. /// - Returns: The HMAC-SHA1 result as Data
  84. private static func generateHMACSHA1(key: Data, data: Data) -> Data {
  85. var hmac = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  86. key.withUnsafeBytes { keyBytes in
  87. data.withUnsafeBytes { dataBytes in
  88. CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), keyBytes.baseAddress, key.count, dataBytes.baseAddress, data.count, &hmac)
  89. }
  90. }
  91. return Data(hmac)
  92. }
  93. }