Skip to content

Instantly share code, notes, and snippets.

@ericlewis
Last active July 29, 2023 11:31
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericlewis/2013e6b126a7a8918286e628403954cd to your computer and use it in GitHub Desktop.
Save ericlewis/2013e6b126a7a8918286e628403954cd to your computer and use it in GitHub Desktop.
A tiny pure swift implementation of HOTP & TOTP
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
}
@ericlewis
Copy link
Author

the identifiable conformance here is probably not necessary in this specific instance, I just copied and pasted from my actual project.

@ericlewis
Copy link
Author

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