Last active
July 29, 2023 11:31
-
-
Save ericlewis/2013e6b126a7a8918286e628403954cd to your computer and use it in GitHub Desktop.
A tiny pure swift implementation of HOTP & TOTP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import CryptoKit | |
protocol PasswordProtocol { | |
var name: String {get} | |
var issuer: String? {get} | |
var image: URL? {get} | |
var generator: GeneratorProtocol {get} | |
} | |
protocol GeneratorProtocol { | |
static func validateDigits(_ digits: Int) throws | |
static func validateFactor(_ factor: Factor) throws | |
static func validateTime(_ timeSinceEpoch: TimeInterval) throws | |
static func validatePeriod(_ period: TimeInterval) throws | |
var factor: FactorProtocol {get} | |
var secret: Data {get} | |
var digits: Int {get} | |
var algorithm: SupportedHashAlgorithm {get} | |
func password(for date: Date) throws -> String | |
} | |
protocol FactorProtocol { | |
func counterValue(for time: Date) throws -> UInt64 | |
} | |
extension GeneratorProtocol { | |
func password(for date: Date) throws -> String { | |
try Self.validateDigits(digits) | |
let counter = try factor.counterValue(for: date) | |
// Ensure the counter value is big-endian | |
var bigCounter = counter.bigEndian | |
// Generate an HMAC value from the key and counter | |
let counterData = Data(bytes: &bigCounter, count: MemoryLayout<UInt64>.size) | |
let hash: Data | |
let key = SymmetricKey(data: secret) | |
func createData(_ ptr: UnsafeRawBufferPointer) -> Data { | |
Data(bytes: ptr.baseAddress!, count: algorithm.hashLength) | |
} | |
switch algorithm { | |
case .sha1: | |
hash = CryptoKit.HMAC<Insecure.SHA1>.authenticationCode(for: counterData, using: key).withUnsafeBytes(createData) | |
case .sha256: | |
hash = CryptoKit.HMAC<SHA256>.authenticationCode(for: counterData, using: key).withUnsafeBytes(createData) | |
case .sha512: | |
hash = CryptoKit.HMAC<SHA512>.authenticationCode(for: counterData, using: key).withUnsafeBytes(createData) | |
} | |
var truncatedHash = hash.withUnsafeBytes { ptr -> UInt32 in | |
let offset = (ptr.last ?? 0x00) & 0x0f | |
let truncatedHashPtr = ptr.baseAddress! + Int(offset) | |
return truncatedHashPtr.bindMemory(to: UInt32.self, capacity: 1).pointee | |
} | |
truncatedHash = UInt32(bigEndian: truncatedHash) | |
truncatedHash &= 0x7fffffff | |
truncatedHash = truncatedHash % UInt32(pow(10, Float(digits))) | |
return String(truncatedHash).padding(toLength: digits, withPad: "0", startingAt: 0) | |
} | |
} | |
extension GeneratorProtocol { | |
static func validateDigits(_ digits: Int) throws { | |
guard (6...8).contains(digits) else { | |
throw OTPError.invalidDigits | |
} | |
} | |
static func validateFactor(_ factor: Factor) throws { | |
switch factor { | |
case .counter: | |
return | |
case .timer(let period): | |
try validatePeriod(period) | |
} | |
} | |
static func validatePeriod(_ period: TimeInterval) throws { | |
guard period > 0 else { | |
throw OTPError.invalidPeriod | |
} | |
} | |
static func validateTime(_ timeSinceEpoch: TimeInterval) throws { | |
guard timeSinceEpoch >= 0 else { | |
throw OTPError.invalidTime | |
} | |
} | |
} | |
enum OTPError: Error { | |
case invalidDigits, invalidPeriod, invalidTime | |
} | |
enum SupportedHashAlgorithm { | |
case sha1, sha256, sha512 | |
var hashLength: Int { | |
switch self { | |
case .sha1: | |
return Insecure.SHA1.byteCount | |
case .sha256: | |
return SHA256.byteCount | |
case .sha512: | |
return SHA512.byteCount | |
} | |
} | |
} | |
enum Factor: FactorProtocol { | |
case counter(UInt64), timer(period: TimeInterval) | |
func counterValue(for time: Date) throws -> UInt64 { | |
switch self { | |
case .counter(let counter): | |
return counter | |
case .timer(let period): | |
let timeSinceEpoch = time.timeIntervalSince1970 | |
try Generator.validateTime(timeSinceEpoch) | |
try Generator.validatePeriod(period) | |
return UInt64(timeSinceEpoch / period) | |
} | |
} | |
} | |
struct Generator: GeneratorProtocol { | |
var factor: FactorProtocol | |
var secret: Data | |
var digits: Int | |
var algorithm: SupportedHashAlgorithm | |
} |
a todo would be to conform to codable as well.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
the identifiable conformance here is probably not necessary in this specific instance, I just copied and pasted from my actual project.