OTP/OTPKit/TOTPKey.swift

111 lines
3.1 KiB
Swift

//
// TOTPKey.swift
// OTPKit
//
// Created by Shadowfacts on 8/21/21.
//
import Foundation
public struct TOTPKey: OTPKey, Equatable, Hashable, Codable {
public let secret: Data
public let period: Int
public let digits: Int
public let algorithm: OTPAlgorithm
public let label: String?
public let issuer: String
// let image: Data?
public init(secret: Data, period: Int, digits: Int, label: String?, issuer: String) {
self.secret = secret
self.period = period
self.digits = digits
self.algorithm = .SHA1
self.label = label
self.issuer = issuer
}
}
public extension TOTPKey {
init?(urlComponents: URLComponents) {
guard urlComponents.scheme == "otpauth",
urlComponents.host == "totp",
let queryItems = urlComponents.queryItems else {
return nil
}
var label: String?
var issuer: String
var secret: Data?
var period: Int = 30
var digits = 6
let path = urlComponents.path.drop(while: { $0 == "/" })
let components = path.components(separatedBy: ":")
if components.count > 1 {
issuer = components[0]
label = components[1]
} else {
issuer = components[0]
}
for item in queryItems {
guard let value = item.value else { continue }
switch item.name.lowercased() {
case "algorithm":
if value.lowercased() != "sha1" {
return nil
}
case "digits":
if let newDigits = Int(value) {
digits = newDigits
}
case "issuer":
issuer = value
case "period":
if let newPeriod = Int(value) {
period = newPeriod
}
case "secret":
if let data = value.base32DecodedData {
secret = data
}
default:
continue
}
}
guard let secret = secret else { return nil }
self.issuer = issuer
self.label = label
self.secret = secret
self.period = period
self.digits = digits
self.algorithm = .SHA1
}
var url: URL {
var components = URLComponents()
components.scheme = "otpauth"
components.host = "totp"
if let label = label, !label.isEmpty {
components.path = "/\(issuer):\(label)"
} else {
components.path = "/\(issuer)"
}
components.queryItems = [
URLQueryItem(name: "algorithm", value: "SHA1"),
URLQueryItem(name: "digits", value: digits.description),
URLQueryItem(name: "issuer", value: issuer),
URLQueryItem(name: "period", value: period.description),
URLQueryItem(name: "secret", value: secret.base32EncodedString),
]
return components.url!
}
}