HOTP (RFC 4226) and TOTP (RFC 6238) one-time password library with Base32, otpauth:// URIs and a CLI.
moon add 1726914517-spec/moon_otp///|
import {
"1726914517-spec/moon_otp",
}///|
/// Generate a new shared secret and enroll a user.
let secret = generate_secret() // 20 random bytes
///|
let otp = {
issuer: "Acme Corp",
account: "alice@example.com",
secret,
algorithm: Sha1,
digits: 6,
period: 30,
}
///|
let uri = otpauth_uri(otp) // QR-code content for an authenticator app
///|
/// Produce the code for the current time, and verify a user-submitted code
/// with one step of clock drift tolerance.
let current = totp_now(Sha1, secret)
///|
let valid = current == submitted_code ||
totp(Sha1, secret, now_seconds - 30) == submitted_code ||
totp(Sha1, secret, now_seconds + 30) == submitted_code
///|
/// The same drift-tolerant check is provided directly:
let ok = totp_verify(Sha1, secret, submitted_code)let key = @utf8.encode("12345678901234567890")
hotp(Sha1, key, 0UL) // "755224"
hotp(Sha1, key, 1UL) // "287082"totp(Sha256, key256, 1111111109UL, digits=8) // "68084774"
totp(Sha512, key512, 1234567890UL, digits=8) // "93441116"///|
let otp = parse_otpauth_uri(uri)
///|
let code = totp_now(
otp.algorithm,
otp.secret,
digits=otp.digits,
period=otp.period,
)///|
let steam_code = steam_guard_now(secret) // e.g. "YHBCW"
///|
let recovery = generate_recovery_codes() // 10 codes like "PJKP-Y94J"///|
/// Client drifted ahead: it submitted codes for counters 2 and 3.
match hotp_resync(Sha1, key, server_counter, code1, code2, window=5) {
Some(new_counter) => // persist new_counter
None => // reject
}moon run cmd/main -- gen --issuer GitHub --account aliceSecret (Base32): 2WEXWAJKI3EFXMXBI3NE2BIAIHE2UNGV
otpauth URI: otpauth://totp/GitHub:alice?secret=...&issuer=GitHub&algorithm=SHA1&digits=6&period=30
Current code: 832873# Current TOTP with seconds remaining in the step
moon run cmd/main -- now --secret 2WEXWAJKI3EFXMXBI3NE2BIAIHE2UNGV
# HOTP at a specific counter
moon run cmd/main -- hotp --secret GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ --counter 0
# Build an otpauth URI from an existing secret
moon run cmd/main -- uri --secret <base32> --issuer GitHub --account alice --algorithm SHA256
# Steam Guard code
moon run cmd/main -- steam --secret <base32>
# One-time recovery codes
moon run cmd/main -- recovery --count 10
# Verify a code (exit 0 on success, 1 on failure)
moon run cmd/main -- verify --secret <base32> --code 123456 --window 1moon testmoon_otp/
├── base32.mbt RFC 4648 Base32
├── sha1.mbt SHA-1
├── sha256.mbt SHA-256
├── sha512.mbt SHA-512
├── digest.mbt HashAlgorithm dispatch
├── hmac.mbt HMAC (RFC 2104)
├── hotp.mbt HOTP (RFC 4226) + resync
├── totp.mbt TOTP (RFC 6238)
├── steam.mbt Steam Guard codes
├── recovery.mbt One-time recovery codes
├── secret.mbt CSPRNG-backed secret generation
├── otpauth.mbt otpauth:// URI build/parse (TOTP + HOTP)
└── cmd/main/ Command line toolpub(all) suberror OtpError {
InvalidDigits(Int)
InvalidPeriod(Int)
EmptySecret
RandomUnavailable
InvalidSecretLength(Int)
InvalidUri(String)
} derive(Debug)pub(all) struct OtpAuth {
issuer : String
account : String
secret : Bytes
algorithm : HashAlgorithm
digits : Int
period : Int
otp_type : OtpKind
counter : UInt64
}fn base32_encode(data : BytesView) -> Stringfn base32_encode_unpadded(data : BytesView) -> Stringfn base32_hex_encode(data : BytesView) -> Stringfn hotp(algorithm : HashAlgorithm, key : Bytes, counter : UInt64, digits? : Int) -> String raise OtpErrorfn hotp_resync(algorithm : HashAlgorithm, key : Bytes, current_counter : UInt64, code1 : String, code2 : String, window? : Int) -> UInt64? raise OtpErrorfn sha1(data : BytesView) -> Bytesfn sha256(data : BytesView) -> Bytesfn sha512(data : BytesView) -> Bytesfn totp(algorithm : HashAlgorithm, key : Bytes, unix_time : UInt64, digits? : Int, period? : Int) -> String raise OtpErrorfn totp_now(algorithm : HashAlgorithm, key : Bytes, digits? : Int, period? : Int) -> String raise OtpErrorfn totp_verify(algorithm : HashAlgorithm, key : Bytes, code : String, window? : Int, digits? : Int, period? : Int) -> Bool raise OtpErrorInstall
Download zipHOTP (RFC 4226) and TOTP (RFC 6238) one-time password library with Base32, otpauth:// URIs and a CLI.