Skip to content

Latest commit

 

History

91 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SwiftyOTP

Swift 6 Platforms

One-time password generation for Swift: RFC 4226 (HOTP) and RFC 6238 (TOTP), plus an @Observable timer that keeps a SwiftUI view in sync with the current code and its countdown.

Pure Swift package, no UI layer. Swift 6 strict concurrency, every public type Sendable.

Installation

.package(url: "https://github.com/swiftverse-dev/SwiftyOTP.git", branch: "main")
.target(name: "MyApp", dependencies: ["SwiftyOTP"])

Quick start

TOTP

import SwiftyOTP

let totp = try TOTPGenerator(seed: .base32("JBSWY3DPEHPK3PXP"))
totp.currentOTP            // "492039" — code for now
totp.otp(at: someDate)     // code for an arbitrary date

HOTP

HOTPGenerator backs TOTPGenerator; counter-based codes are produced internally from the time step. Construct it directly when you need the shared configuration (seed, digits, algorithm).

let hotp = try HOTPGenerator(seed: .hex("3132333435363738393031323334353637383930"), digits: 8, algorithm: .sha256)

Live timer in SwiftUI

struct CodeView: View {
    @State private var timer = try! OTPTimer(seed: .base32("JBSWY3DPEHPK3PXP"))

    var body: some View {
        VStack {
            Text(timer.currentOTP).monospacedDigit()
            ProgressView(value: timer.countdown, total: Double(timer.timeStep))
        }
    }
}

OTPTimer is @MainActor @Observable: read currentOTP, countdown and timeStep directly, no ObservableObject, no publishers.

API

Generators

Type Purpose
Seed Secret input: .base32, .base64, .hex, .data. Decoding is validated at init and throws on malformed input.
HashingAlgorithm .sha1 (default), .sha256, .sha512.
HOTPGenerator RFC 4226 counter-based generator. Exposes seed, digits, algorithm.
TOTPGenerator RFC 6238 time-based generator over HOTPGenerator. Adds timeStep (default 30s), currentOTP, otp(at:).

Both generators throw if digits falls outside 6...8 or if the seed cannot be decoded.

Timer

Type Purpose
Tick One countdown emission: value (seconds left in the window), date, windowChanged.
Countdown Lazy multi-subscriber tick source driven by a Clock<Duration>. ticks returns a fresh AsyncStream<Tick>; the producer loop runs only while at least one subscriber iterates.
OTPTimer @MainActor @Observable driver. start() / stop() are idempotent; startsAutomatically: true by default.
TOTPProvider Protocol for OTP generation: otp(at:), with otp(unixTimestamp:) and otp(intervalSince1970:) provided. TOTPGenerator conforms.
UnixTimestamp .seconds / .milliseconds wrapper for timestamp input.

A new OTP is requested from the provider only when a tick reports a window boundary; intermediate ticks reuse the last value and just move the countdown.

Testing

Inject a TestClock and a deterministic date provider — the library never reads wall time unless you let it:

let clock = TestClock()
let countdown = Countdown(timeStep: 30, clock: clock, dateProvider: { fixedDate })
let timer = OTPTimer(countdown: countdown, totpProvider: generator)

await clock.advance(by: .seconds(1))

Custom providers only need otp(at:):

struct StubProvider: TOTPProvider {
    func otp(at date: Date) -> TOTP { "123456" }
}

Requirements

iOS 17+ / macOS 14+ (Observation framework), Swift 6.2 toolchain.

Dependencies: Base32, swift-clocks.

About

Swift implementation of Hash-based and Time-based OTP generator

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages