Index

API

OneTimePasswords.OneTimePasswordsModule
module OneTimePasswords

A minimal, fast Julia module for generating and verifying counter-based (HOTP, RFC 4226), time-based (TOTP, RFC 6238), and challenge-response (OCRA, RFC 6287) one-time passwords.

Implements RFCs 4226, 6238, and 6287. Compliance not guaranteed. Not audited.

Also provides provisioning URIs and QR-codes for authenticator apps.

Warning

Security Considerations: This module implements only the algorithmic core of HOTP, TOTP, and OCRA. It is stateless and does not include rate limiting, lockout, replay tracking, throttling, or secure memory handling.

Applications using this module must enforce their own controls:

  • Retry and lockout policies
  • Delays or backoff to slow brute-force
  • Replay prevention within sessions or windows
  • Secure storage of shared secrets
  • Secure transport (e.g., TLS)

Secrets are Base32‑encoded immutable Strings. They cannot be zeroized. Use Vector{UInt8} and fill!() if explicit key erasure is required.

Timing and Side‑Channel Notes

  • OTP code comparisons are constant‑time.
  • Base32 decoding and HMAC operations are not guaranteed constant‑time.
  • For multi‑tenant or hostile environments, use a hardened crypto library or HSM.

Examples

julia> using OneTimePasswords

julia> secret = generate_secret();

julia> code = generate(HOTP(), secret, 0; digits=6);

julia> verify(HOTP(), secret, 0, code)
true

julia> account = "alice@example.com";

julia> issuer  = "MyApp";

julia> urilink = uri(HOTP(), secret, account, issuer;
               digits=6, counter=0, algorithm=:SHA1);

julia> svg = qrcode(urilink; format=:svg, size=200, border=2);

julia> tmp_svg = tempname() * "hotp.svg";

julia> open(tmp_svg,"w") do io
           write(io, svg)
       end;

julia> tmp_png = tempname() * "hotp.png";

julia> pngfile = qrcode(urilink; format="png", path=tmp_png);

julia> isfile(pngfile)
true

julia> # qrcode(urilink; format=:ascii, border=1) # Print in the REPL
julia> using OneTimePasswords, Dates

julia> secret = generate_secret();

julia> code = generate(TOTP(), secret; period=Second(30), digits=6);

julia> verify(TOTP(), secret, code; allowed_drift=Second(30))
true

julia> account = "alice@example.com";

julia> issuer  = "MyApp";

julia> urilink = uri(TOTP(), secret, account, issuer; digits=6, 
       period=Second(30));

julia> svg = qrcode(urilink; format=:svg, size=200, border=2);

julia> tmp_svg = tempname() * ".svg";

julia> open(tmp_svg, "w") do io
           write(io, svg)
       end;

julia> tmp_png = tempname() * ".png";

julia> pngfile = qrcode(urilink; format="png", path=tmp_png);

julia> isfile(pngfile)
true

julia> # qrcode(urilink; format=:ascii, border=1) # Print in the REPL
julia> using OneTimePasswords, Dates

julia> secret = generate_secret(64);

julia> suite = "OCRA-1:HOTP-SHA512-8:QA10-T1M";

julia> dt = DateTime(2020,1,1,0,0,30)
2020-01-01T00:00:30

julia> code = generate(OCRA(), secret;
                          suite=suite,
                          challenge="SIG1400000",
                          timestamp=dt,
                          digits=8,
                          algorithm=:SHA512);

julia> verify(OCRA(), secret, code;
               suite=suite,
               challenge="SIG1400000",
               timestamp=dt + Second(60),
               allowed_drift=Second(60),
               digits=8,
               algorithm=:SHA512)
true

julia> account = "alice@example.com";

julia> issuer  = "MyApp";

julia> urilink = uri(OCRA(), secret, "bob", "MyApp";
            suite=suite,
            challenge="SIG1400000",
            timestamp=dt);

julia> svg = qrcode(urilink; format=:svg, size=200, border=2);

julia> tmp_svg = tempname() * ".svg";

julia> open(tmp_svg, "w") do io
           write(io, svg)
       end;

julia> tmp_png = tempname() * ".png";

julia> pngfile = qrcode(urilink; format="png", path=tmp_png);

julia> isfile(pngfile)
true

julia> # qrcode(urilink; format=:ascii, border=1) # Print in the REPL

See also generate_secret, AbstractOTP, HOTP, TOTP, OCRA, generate, verify uri and qrcode.

source
OneTimePasswords._build_ocra_messageFunction
_build_ocra_message(
    suite::AbstractString,
    counter::Union{Nothing,Int}=nothing,
    challenge::AbstractString="",
    passwordhash::AbstractString="",
    session_info::AbstractString="",
    timestamp::Union{Nothing,Int}=nothing)::Vector{UInt8}

Construct the binary “DataInput” value for OCRA (RFC 6287).

The overall layout is:

DataInput = UTF8(suite) ‖ 0x00 ‖ [ C ] ‖ [ Q ] ‖ [ P ] ‖ [ S ] ‖ [ T ]

where

  • Suite: UTF-8 bytes of the suite string, then a 0x00 separator.
  • C: 8-byte big-endian counter (if suite contains “C”).
  • Q: challenge field (numeric/hex/alpha, padded per Qxxx) (if “Q”).
  • P: password-hash left-padded to the HMAC output length (if PSHAx).
  • S: session_info UTF-8 right-padded to Snnn bytes (if Snnn).
  • T: 8-byte big-endian timestamp (if suite contains “T”).
source
OneTimePasswords._dynamic_truncateMethod
_dynamic_truncate(h::Vector{UInt8}, digits::Int)::String

Perform dynamic truncation (as in HOTP) on the HMAC result h and return the OTP as a zero-padded string of length digits.

source
OneTimePasswords._hmacMethod
_hmac(algorithm::Symbol, key::Vector{UInt8}, 
    msg::Vector{UInt8})::Vector{UInt8}

Compute the HMAC of msg using key and the selected algorithm. Supported algorithms are :SHA1, :SHA256, and :SHA512.

source
OneTimePasswords.base32decodeMethod
base32decode(str::AbstractString)::Vector{UInt8}

Decode a Base32 string (per RFC 4648) into a vector of bytes. The input string is not case sensitive and may contain whitespace or padding (=). Throws an exception on invalid characters or impossible encoding.

See also base32encode.

source
OneTimePasswords.exportsvgMethod
exportsvg(
  msg::AbstractString;
  size::Int=240,
  border::Int=4,
  path::Union{Nothing,String}=nothing,
  darkcolor::String="#000",
  lightcolor::String="#fff"
)::String

Generate an SVG <svg>…</svg> QR-code encoding msg. If path is given, also write the SVG to that file.

Examples

julia> using OneTimePasswords

julia> OneTimePasswords.exportsvg("otpauth://totp/bob?...", size=200);

See also qrcode.

source
OneTimePasswords.generateMethod
generate(::HOTP, secret::Union{AbstractString,Vector{UInt8}}, 
    counter::Integer; digits::Int=6, algorithm::Symbol=:SHA1)::String

Compute HOTP for secret and counter (RFC 4226).

Arguments

  • secret: may be either a Base32‐encoded String, or the raw key bytes as Vector{UInt8}.
  • counter: counter value (Integer).
  • digits: code length (default 6).
  • algorithm: :SHA1, :SHA256, or :SHA512.

Examples

julia> using OneTimePasswords

julia> # Base32-encoded String secret

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; #  generate_secret()

julia> generate(HOTP(), secret, 0)
"429658"
julia> using OneTimePasswords, Random

julia> # secret as `Vector{UInt8}; generate_secret_raw()

julia> secret_raw = UInt8[0x67, 0xc0, 0x1e, 0xd3, 0x83, 0xa0, 0x9a, 0x8e, 
       0x1a, 0x74, 0x60, 0x58, 0x1e, 0x40, 0x3b, 0x5a, 0xfd, 0x14, 0x19, 0xc5];

julia> generate(HOTP(), secret_raw, 0)
"429658"

julia> rand!(RandomDevice(), secret_raw);

See also verify(::HOTP).

source
OneTimePasswords.generateMethod
generate(::OCRA, secret::Union{AbstractString,Vector{UInt8}};
         suite::AbstractString = "OCRA-1:HOTP-SHA1-6:QN08",
         counter::Union{Nothing, Integer}=nothing,
         challenge::AbstractString="",
         password::AbstractString="",
         session_info::AbstractString="",
         timestamp::Union{Nothing,Integer}=nothing,
         digits::Int=6,
         algorithm::Symbol=:SHA1)::String

Compute an OCRA one-time password (OTP) according to RFC 6287.

Arguments:

  • secret: Base32-encoded shared secret or the raw key bytes as Vector{UInt8}.
  • suite: OCRA suite definition string.
  • counter: Optional counter value. If omitted, 8 zero bytes are used.
  • challenge: The challenge/question string (e.g. numeric or hex).
  • password: Optional password (P) field.
  • session_info: Optional session information (S) field.
  • timestamp: Optional timestamp (T) as an integer (e.g. Unix time).
  • digits: The number of digits in the OTP.
  • algorithm: The hash algorithm to use (:SHA1, :SHA256, or :SHA512).

Examples

julia> using OneTimePasswords, Dates

julia> # Base32-encoded String secret

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; # generate_secret()

julia> code = generate(OCRA(), secret; suite="OCRA-1:HOTP-SHA1-6:QN08",
            challenge="12345678")
"262022"
julia> using OneTimePasswords, Dates, Random

julia> # secret as `Vector{UInt8}; generate_secret_raw()

julia> secret_raw = UInt8[0x67, 0xc0, 0x1e, 0xd3, 0x83, 0xa0, 0x9a, 0x8e, 
       0x1a, 0x74, 0x60, 0x58, 0x1e, 0x40, 0x3b, 0x5a, 0xfd, 0x14, 0x19, 0xc5];

julia> code = generate(OCRA(), secret_raw; suite="OCRA-1:HOTP-SHA1-6:QN08",
            challenge="12345678")
"262022"

julia> rand!(RandomDevice(), secret_raw);
julia> using OneTimePasswords, Dates

julia> secret = "T6AZ35HKKGWJEUACAUG5MK7T3CBZ5M76Q2GHLMHYOXQEHXKKTATGVH73QBRRW4MBP4P6QKCVMIMMIIBYEY534KZQB6YVK2TE3II3XZA="; # generate_secret(63)

julia> suite = "OCRA-1:HOTP-SHA512-8:QA10-T1M";

julia> dt = DateTime(2020,1,1,0,0,30);

julia> generate(OCRA(), secret;
                 suite=suite,
                 challenge="SIG1400000",
                 timestamp=dt,
                 digits=8,
                 algorithm=:SHA512)
"37236432"

See also verify(::OCRA).

source
OneTimePasswords.generateMethod
generate(::TOTP, secret::Union{AbstractString,Vector{UInt8}};
         time=nothing, period::Union{Period,Integer}=Second(30), 
         digits::Int=6, algorithm::Symbol=:SHA1)::String

Compute a TOTP value for secret at the specified time (RFC 6238), with integer step-countging.

Warning

TOTP (RFC 6238) always uses UTC epoch seconds (since Jan 1, 1970 UTC). If you pass a DateTime without a timezone, it is assumed to be UTC. To avoid mismatches, use Dates.now(UTC) or an explicit Unix timestamp.

Examples

julia> using OneTimePasswords, Dates

julia> # Base32-encoded String secret

julia> secret = "CX6NTW67L7XI3RX7CFUNV4I2ZSXDVSGPLG4KDZ57IJLTM4SOUPNA===="; # generate_secret(32)

julia> generate(TOTP(), secret; digits=8);

julia> dt = DateTime(2020,1,1,0,0,30);

julia> generate(TOTP(), secret; time=dt, digits=7, period=Second(30), 
           algorithm=:SHA256)
"6413619"
julia> using OneTimePasswords, Dates, Random

julia> # secret as `Vector{UInt8}; generate_secret_raw(32)

julia> secret_raw = UInt8[0x15, 0xfc, 0xd9, 0xdb, 0xdf, 0x5f, 0xee, 0x8d, 0xc6, 
       0xff, 0x11, 0x68, 0xda, 0xf1, 0x1a, 0xcc, 0xae, 0x3a, 0xc8, 0xcf, 0x59, 
       0xb8, 0xa1, 0xe7, 0xbf, 0x42, 0x57, 0x36, 0x72, 0x4e, 0xa3, 0xda];

julia> generate(TOTP(), secret_raw; digits=8);

julia> dt = DateTime(2020,1,1,0,0,30);

julia> generate(TOTP(), secret_raw; time=dt, digits=7, period=Second(30), 
           algorithm=:SHA256)
"6413619"

julia> rand!(RandomDevice(), secret_raw);

See also verify(::TOTP).

source
OneTimePasswords.generate_secret_rawFunction
generate_secret_raw(n::Integer=20) -> Vector{UInt8}

Generate n cryptographically secure random bytes (OS RNG). Throws ArgumentError for non-positive or too-large n.

Examples

julia> using OneTimePasswords, Random

julia> secret_raw = generate_secret_raw();

julia> rand!(RandomDevice(), secret_raw);

See also generate_secret.

source
OneTimePasswords.qrcodeMethod
qrcode(
  uri::AbstractString;
  format::Union{Symbol,String} = :svg,
  size::Int = 240,
  border::Int = 4,
  path::Union{Nothing,String} = nothing,
  darkcolor::String = "#000",
  lightcolor::String = "#fff"
)::Union{String,String}

Generate a QR-code for a provisioning uri. Supports:

  • SVG (:svg, returns SVG text),
  • Bitmap ("png", "jpg", "gif", writes to path) and
  • Terminal ASCII (:ascii, prints a scannable QR code in the REPL).

Examples

julia> using OneTimePasswords, Dates

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; # generate_secret()

julia> code = generate(TOTP(), secret; period=Second(30), digits=6);

julia> verify(TOTP(), secret, code; allowed_drift=Second(30))
true

julia> urilink = uri(TOTP(), secret, "bob@example.com", "MyApp"; digits=6, 
       period=Second(30));

julia> svg = qrcode(urilink; format=:svg, size=200, border=2);

julia> tmp_svg = tempname() * ".svg";

julia> open(tmp_svg, "w") do io
           write(io, svg)
       end;

julia> tmp_png = tempname() * ".png";

julia> pngfile = qrcode(urilink; format="png", path=tmp_png);

julia> isfile(pngfile)
true

julia> # qrcode(urilink; format=:ascii, border=1) # Print in the REPL

See also uri and exportsvg.

source
OneTimePasswords.uriMethod
uri(::HOTP, secret::AbstractString,
    account::AbstractString, issuer::AbstractString;
    digits::Int=6, counter::Integer=0,
    algorithm::Symbol=:SHA1)::String

Return an otpauth://hotp/... provisioning URI for HOTP.

Examples

julia> using OneTimePasswords

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; # generate_secret()

julia> uri(HOTP(), secret, "bob@example.com", "MyApp"; counter=5)
"otpauth://hotp/MyApp%3Abob%40example.com?secret=M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF&issuer=MyApp&digits=6&counter=5&algorithm=SHA1"

See also qrcode.

source
OneTimePasswords.uriMethod
uri(::OCRA, secret::AbstractString,
    account::AbstractString, issuer::AbstractString;
    suite::AbstractString="OCRA-1:HOTP-SHA1-6:QN08",
    digits::Int=6,
    algorithm::Symbol=:SHA1,
    counter::Union{Nothing,Int}=nothing,
    challenge::AbstractString="",
    password::AbstractString="",
    session_info::AbstractString="",
    timestamp::Union{Nothing,Int,DateTime}=nothing
   )::String

Return an otpauth://ocra/... provisioning URI for OCRA.

The label will be issuer:account (percent-escaped), and the query string will include secret, issuer, suite, digits, algorithm, and any of the optional fields you pass in.

julia> using OneTimePasswords

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; # generate_secret()

julia> uri(OCRA(), secret, "bob@example.com", "MyApp");

julia> uri(OCRA(), secret, "alice@site.com", "YourOrg";
            suite="OCRA-1:HOTP-SHA256-8:C-QN08-PSHA1",
            digits=8,
            algorithm=:SHA256,
            counter=5,
            challenge="12345678",
            password="7110eda4d09e062aa5e4a390b0a572ac0d2c0220");

See also qrcode.

source
OneTimePasswords.uriMethod
uri(::TOTP, secret::AbstractString,
    account::AbstractString, issuer::AbstractString;
    digits::Int=6, period::Period=Second(30))::String

Return an otpauth://totp/... provisioning URI for TOTP.

Examples

julia> using OneTimePasswords

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; # generate_secret()

julia> uri(TOTP(), secret, "alice@example.com", "MyApp");

julia> uri(TOTP(), secret, "bob@site.com", "YourApp"; digits=8, period=60)
"otpauth://totp/YourApp%3Abob%40site.com?secret=M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF&issuer=YourApp&digits=8&period=60"

See also qrcode.

source
OneTimePasswords.verifyMethod
verify(::HOTP, secret::Union{AbstractString,Vector{UInt8}}, 
       counter::Integer, code::AbstractString; digits::Int=6,
       algorithm::Symbol=:SHA1)::Bool

Return true if code matches the HOTP for secret and counter.

Arguments are the same as for generate(::HOTP). code is compared in constant time to mitigate timing attacks.

Warning

Counter replay: HOTP (RFC 4226) requires that each counter value MUST be used at most once. This library does not manage counters; it only checks whether a single code matches. Your application/server must track and advance the counter and reject any reused codes to prevent replay attacks.

Examples

julia> using OneTimePasswords

julia> # Base32-encoded String secret

julia> secret = generate_secret();

julia> code = generate(HOTP(), secret, 123; digits=6);

julia> verify(HOTP(), secret, 123, code)
true

jldoctest julia> using OneTimePasswords, Random

julia> # secret as `Vector{UInt8}; generatesecretraw()

julia> secretraw = generatesecret_raw();

julia> code2 = generate(HOTP(), secret_raw, 123);

julia> verify(HOTP(), secret_raw, 123, code2) true

julia> rand!(RandomDevice(), secret_raw); ```

See also generate(::HOTP).

source
OneTimePasswords.verifyMethod
verify(::OCRA, secret::AbstractString, code::AbstractString;
       suite::AbstractString = "OCRA-1:HOTP-SHA1-6:QN08",
       counter::Union{Nothing, Integer}=nothing,
       challenge::AbstractString="",
       password::AbstractString="",
       session_info::AbstractString="",
       timestamp::Union{Nothing,Integer,DateTime}=nothing,
       allowed_drift::Period=Second(0),
       digits::Int=6,
       algorithm::Symbol=:SHA1)::Bool

Verify that the provided code matches the OCRA OTP.

Examples

julia> using OneTimePasswords, Dates

julia> # Base32-encoded String secret

julia> secret = generate_secret();

julia> code = generate(OCRA(), secret; challenge="12345678");

julia> verify(OCRA(), secret, code; challenge="12345678")
true
julia> using OneTimePasswords, Dates, Random

julia> # secret as `Vector{UInt8}; generate_secret_raw()

julia> secret_raw = generate_secret_raw();

julia> code = generate(OCRA(), secret_raw; challenge="12345678");

julia> verify(OCRA(), secret_raw, code; challenge="12345678")
true

julia> rand!(RandomDevice(), secret_raw);
julia> using OneTimePasswords, Dates

julia> secret = generate_secret();

julia> suite = "OCRA-1:HOTP-SHA512-8:QA10-T1M";

julia> dt = DateTime(2020,1,1,0,0,30)
2020-01-01T00:00:30

julia> secret = "T6AZ35HKKGWJEUACAUG5MK7T3CBZ5M76Q2GHLMHYOXQEHXKKTATGVH73QBRRW4MBP4P6QKCVMIMMIIBYEY534KZQB6YVK2TE3II3XZA="; # generate_secret(63)

julia> code2 = generate(OCRA(), secret;
                          suite=suite,
                          challenge="SIG1400000",
                          timestamp=dt,
                          digits=8,
                          algorithm=:SHA512);

julia> verify(OCRA(), secret, code2;
               suite=suite,
               challenge="SIG1400000",
               timestamp=dt + Second(60),
               allowed_drift=Second(60),
               digits=8,
               algorithm=:SHA512)
true
source
OneTimePasswords.verifyMethod
verify(::TOTP, secret, code::AbstractString;
       period::Period=Second(30), allowed_drift::Period=Second(30),
       digits::Int=6, time=nothing,
       algorithm::Symbol=:SHA1)::Bool

Return true if code is a valid TOTP for secret at time, allowing ±allowed_drift time window.

Warning

TOTP (RFC 6238) always uses UTC epoch seconds (since Jan 1, 1970 UTC). If you pass a DateTime without a timezone, it is assumed to be UTC. To avoid mismatches, use Dates.now(UTC) or an explicit Unix timestamp.

Warning

Allowed drift window: By default, verification allows ±30 seconds (one time-step) of drift. Increasing this allowed_drift widens the acceptance window and makes brute forcing easier. For best security, keep the drift window as small as your deployment can tolerate.

Examples

julia> using OneTimePasswords, Dates

julia> # Base32-encoded String secret

julia> secret = "M7AB5U4DUCNI4GTUMBMB4QB3LL6RIGOF"; # generate_secret()

julia> dt = DateTime(2022,1,1,0,0,30);

julia> code = generate(TOTP(), secret; time=dt, digits=8);

julia> verify(TOTP(), secret, code; time=dt, digits=8)
true

julia> verify(TOTP(), secret, code; time=dt+Minute(1), digits=8,
                           allowed_drift=Second(60))
true

julia> verify(TOTP(), secret, code; time=dt+Minute(1), digits=8,
                           allowed_drift=Second(30))
false
julia> using OneTimePasswords, Dates, Random

julia> # secret as `Vector{UInt8}; generate_secret_raw()

julia> dt = DateTime(2022,1,1,0,0,30);

julia> secret_raw = UInt8[0x67, 0xc0, 0x1e, 0xd3, 0x83, 0xa0, 0x9a, 0x8e, 
       0x1a, 0x74, 0x60, 0x58, 0x1e, 0x40, 0x3b, 0x5a, 0xfd, 0x14, 0x19, 0xc5];

julia> code = generate(TOTP(), secret_raw; time=dt, digits=8);

julia> verify(TOTP(), secret_raw, code; time=dt, digits=8)
true

julia> rand!(RandomDevice(), secret_raw);

See also generate(::TOTP).

source