OTP Code Generator — Swift source
Generate time-based one-time passwords (RFC 6238 TOTP) from a Base32 secret, with selectable algorithm, digit count, and period - updating live, entirely in your browser.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// otp-code-generator — Swift port: TOTP (RFC 6238) / HOTP (RFC 4226) generator.
//
// Display port of the CosmoDev OTP Code Generator tool — same contract as
// cli/otp-code-generator/otp-code-generator.go (the live Go twin) and
// src/lib/otp.ts (canonical TypeScript, which wraps `otpauth`). HMAC-SHA1
// comes from CryptoKit; base32 is hand-rolled. Only the SHA1 path is ported
// — the TS default and the algorithm every published RFC 6238/4226 test
// vector uses; SHA256/512 follow the same HMAC construction in the Go twin.
import CryptoKit
import Foundation
/// TOTP options — mirrors `Options` in the Go twin.
public struct TotpOptions {
/// Base32 (RFC 4648); spaces/case tolerated.
public var secret: String
/// Digits; <= 0 falls back to the TS default 6.
public var digits: Int
/// Period in seconds; <= 0 falls back to the TS default 30.
public var period: Int
public init(secret: String, digits: Int = 6, period: Int = 30) {
self.secret = secret
self.digits = digits
self.period = period
}
}
public enum Otp {
/// TOTP at `timestampMs` (ms since epoch) — `Generate()` in the Go twin
/// (RFC 6238). nil on an invalid secret, like generateTotp() in the TS lib.
public static func generate(_ opts: TotpOptions, timestampMs: Int64) -> String? {
let digits = opts.digits > 0 ? opts.digits : 6 // withDefaults in the Go twin
let period = opts.period > 0 ? opts.period : 30
guard let key = decodeSecret(opts.secret) else { return nil }
let counter = UInt64(bitPattern: timestampMs) / 1000 / UInt64(period) // RFC 6238 §4.2
var msg = [UInt8](repeating: 0, count: 8)
for i in 0..<8 { msg[i] = UInt8(truncatingIfNeeded: counter >> (8 * (7 - i))) } // RFC 4226 §5.2
// Insecure.SHA1 is CryptoKit's SHA1 (namespaced to flag its crypto age).
let mac = Array(HMAC<Insecure.SHA1>.authenticationCode(for: msg, using: SymmetricKey(data: key)))
let offset = Int(mac[mac.count - 1] & 0x0f) // dynamic truncation, RFC 4226 §5.4
let b0 = UInt32(mac[offset] & 0x7f)
let b1 = UInt32(mac[offset + 1])
let b2 = UInt32(mac[offset + 2])
let b3 = UInt32(mac[offset + 3])
let bin = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
var mod: UInt64 = 1
for _ in 0..<digits { mod *= 10 }
var code = UInt64(bin) % mod
var chars = [Character](repeating: "0", count: digits)
var i = digits - 1
while i >= 0 {
chars[i] = Character(String(code % 10))
code /= 10
i -= 1
}
return String(chars)
}
/// `Validate()` in the Go twin: the current period and ±1 adjacent periods
/// (otpauth's default window=1). `constantTimeEquals` mirrors hmac.Equal.
public static func validate(_ token: String, _ opts: TotpOptions, timestampMs: Int64) -> Bool {
let period = opts.period > 0 ? opts.period : 30
let window = Int64(period) * 1000
for ts in [timestampMs, timestampMs - window, timestampMs + window] {
if let candidate = generate(opts, timestampMs: ts),
constantTimeEquals(candidate, token) {
return true
}
}
return false
}
/// decodeSecret() in the Go twin: strip whitespace, uppercase, strip
/// trailing '=' padding, then RFC 4648 decode. nil when the secret is
/// invalid.
static func decodeSecret(_ secret: String) -> [UInt8]? {
var norm = ""
for c in secret where !c.isWhitespace {
norm.append(Character(c.uppercased()))
}
while norm.last == "=" { norm.removeLast() }
var bytes: [UInt8] = []
var acc = 0, bits = 0
for c in norm {
guard let a = c.asciiValue else { return nil }
let v: Int
if a >= 65 && a <= 90 { v = Int(a) - 65 } // A-Z
else if a >= 50 && a <= 55 { v = Int(a) - 50 + 26 } // 2-7
else { return nil }
acc = (acc << 5) | v
bits += 5
if bits >= 8 {
bits -= 8
bytes.append(UInt8((acc >> bits) & 0xff))
}
}
return bytes
}
/// XOR-fold compare that does not short-circuit — the Swift counterpart of
/// the Go twin's constant-time hmac.Equal.
static func constantTimeEquals(_ a: String, _ b: String) -> Bool {
let x = Array(a.utf8), y = Array(b.utf8)
if x.count != y.count { return false }
var diff: UInt8 = 0
for i in x.indices { diff |= x[i] ^ y[i] }
return diff == 0
}
}
// RFC 6238 appendix-B vectors (SHA1, 8 digits, 30s):
// secret = base32("12345678901234567890").
let o8 = TotpOptions(secret: "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", digits: 8)
for (ts, want) in [(59_000, "94287082"),
(1_111_111_109_000, "07081804"),
(1_234_567_890_000, "89005924"),
(2_000_000_000_000, "69279037")] {
print("ts=\(ts) token=\(Otp.generate(o8, timestampMs: Int64(ts)) ?? "invalid") want=\(want)")
}
// Tool defaults (6 digits) + whitespace/case-tolerant secret entry.
let o6 = TotpOptions(secret: "gezd gnbv gy3t qojq gezd gnbv gy3t qojq")
let token = Otp.generate(o6, timestampMs: 59_000) ?? "invalid"
print("6-digit=\(token) validate(now)=\(Otp.validate(token, o6, timestampMs: 59_000)) " +
"validate(+1)=\(Otp.validate(token, o6, timestampMs: 89_000)) " +
"validate(wrong)=\(Otp.validate("123789", o6, timestampMs: 59_000))")
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →