TOTPGenerator.swift 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import Foundation
  2. import CommonCrypto
  3. class TOTPGenerator {
  4. static func generateTOTP(base32Secret: String, digits: Int, timeStepSeconds: Int) throws -> String {
  5. let secret = base32Decode(base32Secret)
  6. var timeStep = UInt64(Date().timeIntervalSince1970) / UInt64(timeStepSeconds)
  7. var data = [UInt8](repeating: 0, count: 8)
  8. // convert timeStep to byte[8]
  9. for i in stride(from: 7, through: 0, by: -1) {
  10. data[i] = UInt8(timeStep & 0xFF)
  11. timeStep >>= 8
  12. }
  13. let hash = try hmacSha1(key: secret, data: data)
  14. // dynamic truncation
  15. let offset = Int(hash[hash.count - 1] & 0x0F)
  16. let binary =
  17. ((Int(hash[offset]) & 0x7f) << 24) |
  18. ((Int(hash[offset + 1]) & 0xff) << 16) |
  19. ((Int(hash[offset + 2]) & 0xff) << 8) |
  20. (Int(hash[offset + 3]) & 0xff)
  21. let otp = binary % Int(pow(10.0, Double(digits)))
  22. return String(format: "%0\(digits)d", otp)
  23. }
  24. static func generateHOTP(base32Secret: String, digits: Int, counter: UInt64) throws -> String {
  25. let secret = base32Decode(base32Secret)
  26. var counterValue = counter
  27. var data = [UInt8](repeating: 0, count: 8)
  28. // convert counter to byte[8]
  29. for i in stride(from: 7, through: 0, by: -1) {
  30. data[i] = UInt8(counterValue & 0xFF)
  31. counterValue >>= 8
  32. }
  33. let hash = try hmacSha1(key: secret, data: data)
  34. // dynamic truncation
  35. let offset = Int(hash[hash.count - 1] & 0x0F)
  36. let binary =
  37. ((Int(hash[offset]) & 0x7f) << 24) |
  38. ((Int(hash[offset + 1]) & 0xff) << 16) |
  39. ((Int(hash[offset + 2]) & 0xff) << 8) |
  40. (Int(hash[offset + 3]) & 0xff)
  41. let otp = binary % Int(pow(10.0, Double(digits)))
  42. return String(format: "%0\(digits)d", otp)
  43. }
  44. // MARK: - Base32 Decoder
  45. private static func base32Decode(_ base32: String) -> [UInt8] {
  46. let base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
  47. let clean = base32.replacingOccurrences(of: "=", with: "").uppercased()
  48. var bytes = [UInt8]()
  49. var buffer = 0
  50. var bitsLeft = 0
  51. for char in clean {
  52. guard let val = base32Chars.firstIndex(of: char) else { continue }
  53. let idx = base32Chars.distance(from: base32Chars.startIndex, to: val)
  54. buffer <<= 5
  55. buffer |= idx
  56. bitsLeft += 5
  57. if bitsLeft >= 8 {
  58. let byte = UInt8((buffer >> (bitsLeft - 8)) & 0xFF)
  59. bytes.append(byte)
  60. bitsLeft -= 8
  61. }
  62. }
  63. return bytes
  64. }
  65. // MARK: - HMAC-SHA1
  66. private static func hmacSha1(key: [UInt8], data: [UInt8]) throws -> [UInt8] {
  67. var macData = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
  68. CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), key, key.count, data, data.count, &macData)
  69. return macData
  70. }
  71. }