Sign in

    moon_otp

    HOTP (RFC 4226) and TOTP (RFC 6238) one-time password library with Base32, otpauth:// URIs and a CLI.

    otp
    totp
    hotp
    hmac
    2fa
    base32
    otpauth
    Download zip
    Version
    0.3.1
    License
    Apache-2.0
    Last updated
    14 days ago
    Downloads
    14

    #moon_otp

    A one-time password (OTP) library for MoonBit, implementing HOTP and TOTP from the ground up — no external crypto dependencies.

    • HOTP — HMAC-based one-time passwords, RFC 4226
    • TOTP — time-based one-time passwords, RFC 6238
    • HMAC — keyed hashing, RFC 2104, over SHA-1 / SHA-256 / SHA-512
    • SHA-1 / SHA-256 / SHA-512 — FIPS 180-4 hash implementations
    • Base32 — RFC 4648, standard and Extended Hex alphabets
    • otpauth:// URIs — build and parse the de-facto authenticator format
    • Steam Guard — five-character Steam mobile authenticator codes
    • Recovery codes — one-time backup codes for account recovery
    • HOTP resync — counter resynchronization per RFC 4226 §7.4
    • Secure secret generation — backed by the platform CSPRNG
    • A small command line tool for enrollment and code generation

    Every algorithm is verified against the official RFC / NIST test vectors (40 tests).

    #Install

    moon add 1726914517-spec/moon_otp

    Then import the root package:

    ///|
    import {
    "1726914517-spec/moon_otp",
    }

    #Library usage

    #TOTP (the common case)

    ///|
    /// 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)

    #HOTP

    let key = @utf8.encode("12345678901234567890")
    hotp(Sha1, key, 0UL) // "755224"
    hotp(Sha1, key, 1UL) // "287082"

    #TOTP with SHA-256/SHA-512 and 8 digits

    totp(Sha256, key256, 1111111109UL, digits=8) // "68084774"
    totp(Sha512, key512, 1234567890UL, digits=8) // "93441116"

    #Parsing an otpauth URI

    ///|
    let otp = parse_otpauth_uri(uri)

    ///|
    let code = totp_now(
    otp.algorithm,
    otp.secret,
    digits=otp.digits,
    period=otp.period,
    )

    #Steam Guard and recovery codes

    ///|
    let steam_code = steam_guard_now(secret) // e.g. "YHBCW"

    ///|
    let recovery = generate_recovery_codes() // 10 codes like "PJKP-Y94J"

    #HOTP counter resynchronization (RFC 4226 §7.4)

    ///|
    /// 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
    }

    All fallible functions raise the OtpError error set (invalid digits, empty secret, malformed URI, unavailable random source, ...).

    #CLI

    Build and run with the MoonBit toolchain:

    moon run cmd/main -- gen --issuer GitHub --account alice

    Secret (Base32): 2WEXWAJKI3EFXMXBI3NE2BIAIHE2UNGV otpauth URI: otpauth://totp/GitHub:alice?secret=...&issuer=GitHub&algorithm=SHA1&digits=6&period=30 Current code: 832873

    Other commands:

    # 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 1

    #Testing

    moon test

    The suite contains 40 tests covering:

    • FIPS 180-4 / NIST vectors for all three hashes (empty, "abc", two-block)
    • HMAC vectors from RFC 2202 (SHA-1) and RFC 4231 (SHA-256/512)
    • The full HOTP Appendix D sequence (counters 0–9)
    • The complete TOTP Appendix B table for SHA-1/256/512, including the 8-digit values
    • Base32 RFC 4648 vectors and strict padding validation
    • otpauth URI round trips for both TOTP and HOTP, and malformed-URI errors
    • Steam Guard format, recovery codes and HOTP resynchronization
    • Drift-tolerant totp_verify, input validation and secret generation

    #Project layout

    moon_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 tool

    #Relation to existing packages

    moon_otp is a focused OTP library. Related registry packages overlap with individual building blocks but do not provide a complete, dedicated OTP solution:

    • Q30399/moonvault is a broad password-hashing/crypto suite. Its published 0.1.0 covers TOTP generation/verification and URI building (with no HOTP function); its unreleased main branch adds HOTP, but totp_now and totp_verify pass a hardcoded timestamp 0 and never read the clock. moon_otp provides the missing, correct pieces:
      • otpauth:// URI parsing — moonvault only builds URIs (without algorithm/digits/period parameters or percent encoding);
      • a command line tool with gen/now/hotp/uri;
      • correct current-time TOTP with a configurable drift window;
      • RFC-verified HOTP and the complete RFC vector suites (HOTP Appendix D, TOTP Appendix B across SHA-1/256/512).
    • cc06b/mooncry is a broad, zero-dependency cryptographic primitives library (hashes, AEAD, KDFs, asymmetric and post-quantum algorithms, ...). Its OTP surface is generation only — hotp/totp plus SHA-256/512 variants — with no Base32, no otpauth:// build or parse, no current-time/verify APIs, no Steam Guard, recovery codes, resynchronization, or CLI. moon_otp is a dedicated, end-to-end OTP solution covering enrollment (secret generation + otpauth), distribution (CLI) and verification.
    • Tigls/mb-hmac provides HMAC; moon_otp uses its own HMAC because the OTP layer requires all three hash algorithms and tight control of block sizes.
    • yyjeqhc/base32 and Lfan-ke/basex provide Base32; moon_otp includes its own decoder to validate OTP secrets without an extra dependency.

    #License

    Licensed under the Apache License, Version 2.0. See LICENSE.

    Base32Error

    pub(all) suberror Base32Error {
    InvalidCharacter(Int, Char)
    InvalidPadding(Int)
    } derive(
    Debug
    )

    Error returned when Base32 decoding fails.

    OtpError

    pub(all) suberror OtpError {
    InvalidDigits(Int)
    InvalidPeriod(Int)
    EmptySecret
    RandomUnavailable
    InvalidSecretLength(Int)
    InvalidUri(String)
    } derive(
    Debug
    )

    Errors returned by the OTP routines.

    HashAlgorithm

    pub(all) enum HashAlgorithm {
    Sha1
    Sha256
    Sha512
    } derive(Eq,
    Debug
    )

    Cryptographic hash algorithms supported by this package.

    OtpAuth

    pub(all) struct OtpAuth {
    issuer : String
    account : String
    secret : Bytes
    algorithm : HashAlgorithm
    digits : Int
    period : Int
    otp_type : OtpKind
    counter : UInt64
    }

    Parameters of an otpauth enrollment URI.

    OtpAuth::new

    fn OtpAuth::new(issuer : String, account : String, secret : Bytes) -> OtpAuth

    Create the default parameter set for a secret (TOTP, SHA-1, 6 digits, 30 s).

    OtpKind

    pub(all) enum OtpKind {
    Totp
    Hotp
    } derive(Eq,
    Debug
    )

    OTP type carried by an otpauth URI.

    algorithm_name

    fn algorithm_name(algo : HashAlgorithm) -> String

    Canonical name used in otpauth:// URIs ("SHA1", "SHA256", "SHA512").

    base32_decode

    fn base32_decode(input : String) -> Bytes raise Base32Error

    Decode a standard Base32 string. Padding is optional but validated when present. Whitespace is not accepted; strip it beforehand if needed.

    base32_encode

    fn base32_encode(data : BytesView) -> String

    Encode bytes with the standard Base32 alphabet, including "=" padding.

    base32_encode_unpadded

    fn base32_encode_unpadded(data : BytesView) -> String

    Encode bytes with the standard Base32 alphabet, omitting padding. This is the form used by Google Authenticator / otpauth secret keys.

    base32_hex_decode

    fn base32_hex_decode(input : String) -> Bytes raise Base32Error

    Decode a Base32 Extended Hex string (RFC 4648 section 7).

    base32_hex_encode

    fn base32_hex_encode(data : BytesView) -> String

    Encode bytes with the Base32 Extended Hex alphabet (RFC 4648 section 7).

    block_size

    fn block_size(algo : HashAlgorithm) -> Int

    Block size in bytes of the algorithm's compression function.

    digest

    fn digest(algo : HashAlgorithm, data : BytesView) -> Bytes

    One-shot hash: compute the digest of all of data.

    generate_recovery_codes

    fn generate_recovery_codes(count? : Int, groups? : Int, group_size? : Int) -> Array[String] raise OtpError

    Generate count unique recovery codes, each split into groups groups of group_size characters (default 10 codes of the form XXXX-XXXX).

    generate_secret

    fn generate_secret(length? : Int) -> Bytes raise OtpError

    Generate length cryptographically random secret bytes.

    generate_secret_base32

    fn generate_secret_base32(length? : Int) -> String raise OtpError

    Generate a secret and return it as an unpadded RFC 4648 Base32 string, the format used by Google Authenticator / most 2FA enrollment flows.

    hmac

    fn hmac(algo : HashAlgorithm, key : BytesView, message : BytesView) -> Bytes

    Compute HMAC: H((K' xor opad) || H((K' xor ipad) || message)).

    Keys longer than the hash block size are first replaced by their digest. Shorter keys are zero-padded to the block size.

    hotp

    fn hotp(algorithm : HashAlgorithm, key : Bytes, counter : UInt64, digits? : Int) -> String raise OtpError

    Compute an HOTP code.

    • algorithm: hash function used for the HMAC.
    • key: shared secret (RFC test keys are ASCII; real deployments use random bytes).
    • counter: 8-byte moving factor.
    • digits: number of output digits (RFC 4226 recommends 6; supported 1..8).

    hotp_resync

    fn hotp_resync(algorithm : HashAlgorithm, key : Bytes, current_counter : UInt64, code1 : String, code2 : String, window? : Int) -> UInt64? raise OtpError

    Resynchronize an HOTP client counter per RFC 4226 §7.4.

    The client submits two consecutive codes; the server searches a look-ahead window starting from its stored counter. Returns the counter matching the first code, or None if the pair was not found.

    otpauth_uri

    fn otpauth_uri(otp : OtpAuth) -> String raise OtpError

    Build an otpauth:// URI (TOTP or HOTP).

    output_size

    fn output_size(algo : HashAlgorithm) -> Int

    Output digest length in bytes.

    parse_algorithm

    fn parse_algorithm(name : String) -> HashAlgorithm?

    Parse an algorithm name as it appears in otpauth URIs. Case insensitive. Unknown names return None.

    parse_otpauth_uri

    fn parse_otpauth_uri(uri : String) -> OtpAuth raise OtpError

    Parse an otpauth://totp/ or otpauth://hotp/ URI.

    sha1

    fn sha1(data : BytesView) -> Bytes

    Compute the SHA-1 digest of data, returning 20 raw bytes.

    sha256

    fn sha256(data : BytesView) -> Bytes

    Compute the SHA-256 digest of data, returning 32 raw bytes.

    sha512

    fn sha512(data : BytesView) -> Bytes

    Compute the SHA-512 digest of data, returning 64 raw bytes.

    steam_guard

    fn steam_guard(key : Bytes, unix_time : UInt64, period? : Int) -> String raise OtpError

    Compute a Steam Guard code from a Unix timestamp in seconds.

    steam_guard_now

    fn steam_guard_now(key : Bytes) -> String raise OtpError

    Compute the Steam Guard code for the current system time.

    totp

    fn totp(algorithm : HashAlgorithm, key : Bytes, unix_time : UInt64, digits? : Int, period? : Int) -> String raise OtpError

    Compute a TOTP code from a Unix timestamp in seconds.

    • unix_time: number of seconds since 1970-01-01.
    • digits: output digits (RFC 6238 uses 6 or 8).
    • period: time step in seconds (default 30).

    totp_now

    fn totp_now(algorithm : HashAlgorithm, key : Bytes, digits? : Int, period? : Int) -> String raise OtpError

    Compute the TOTP code for the current system time.

    totp_verify

    fn totp_verify(algorithm : HashAlgorithm, key : Bytes, code : String, window? : Int, digits? : Int, period? : Int) -> Bool raise OtpError

    Verify a submitted TOTP code, accepting window time steps of clock drift in each direction (default 1 step).