Index
OneTimePasswords.OneTimePasswordsOneTimePasswords.AbstractOTPOneTimePasswords.HOTPOneTimePasswords.OCRAOneTimePasswords.TOTPOneTimePasswords._build_ocra_messageOneTimePasswords._dynamic_truncateOneTimePasswords._hmacOneTimePasswords.base32decodeOneTimePasswords.base32encodeOneTimePasswords.exportsvgOneTimePasswords.generateOneTimePasswords.generateOneTimePasswords.generateOneTimePasswords.generate_secretOneTimePasswords.generate_secret_rawOneTimePasswords.qrcodeOneTimePasswords.uriOneTimePasswords.uriOneTimePasswords.uriOneTimePasswords.verifyOneTimePasswords.verifyOneTimePasswords.verify
API
OneTimePasswords.OneTimePasswords — Modulemodule OneTimePasswordsA 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.
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 REPLjulia> 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 REPLjulia> 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 REPLSee also generate_secret, AbstractOTP, HOTP, TOTP, OCRA, generate, verify uri and qrcode.
OneTimePasswords.AbstractOTP — TypeAbstractOTPAbstract supertype for one-time-password generators.
OneTimePasswords.HOTP — TypeHOTP()Counter-based OTP (RFC 4226).
OneTimePasswords.OCRA — TypeOCRA()The OATH Challenge-Response Algorithm (RFC 6287).
OneTimePasswords.TOTP — TypeTOTP()Time-based OTP (RFC 6238).
OneTimePasswords._build_ocra_message — Function_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”).
OneTimePasswords._dynamic_truncate — Method_dynamic_truncate(h::Vector{UInt8}, digits::Int)::StringPerform dynamic truncation (as in HOTP) on the HMAC result h and return the OTP as a zero-padded string of length digits.
OneTimePasswords._hmac — Method_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.
OneTimePasswords.base32decode — Methodbase32decode(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.
OneTimePasswords.base32encode — Methodbase32encode(bytes::Vector{UInt8})::StringEncode a byte vector to a Base32 string according to RFC 4648. Result is always ASCII, using uppercase A-Z and digits 2-7, with = padding.
See also base32decode.
OneTimePasswords.exportsvg — Methodexportsvg(
msg::AbstractString;
size::Int=240,
border::Int=4,
path::Union{Nothing,String}=nothing,
darkcolor::String="#000",
lightcolor::String="#fff"
)::StringGenerate 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.
OneTimePasswords.generate — Methodgenerate(::HOTP, secret::Union{AbstractString,Vector{UInt8}},
counter::Integer; digits::Int=6, algorithm::Symbol=:SHA1)::StringCompute HOTP for secret and counter (RFC 4226).
Arguments
secret: may be either a Base32‐encodedString, or the raw key bytes asVector{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).
OneTimePasswords.generate — Methodgenerate(::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)::StringCompute an OCRA one-time password (OTP) according to RFC 6287.
Arguments:
secret: Base32-encoded shared secret or the raw key bytes asVector{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).
OneTimePasswords.generate — Methodgenerate(::TOTP, secret::Union{AbstractString,Vector{UInt8}};
time=nothing, period::Union{Period,Integer}=Second(30),
digits::Int=6, algorithm::Symbol=:SHA1)::StringCompute a TOTP value for secret at the specified time (RFC 6238), with integer step-countging.
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).
OneTimePasswords.generate_secret — Functiongenerate_secret([length::Int=20])::StringGenerate a cryptographically-strong random secret (byte length length) and return it Base32-encoded. Default is 20 bytes (good for SHA1/TOTP).
Examples
julia> using OneTimePasswords
julia> secret = generate_secret();See also base32encode, base32decode and and generate_secret_raw.
OneTimePasswords.generate_secret_raw — Functiongenerate_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.
OneTimePasswords.qrcode — Methodqrcode(
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 topath) 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 REPLOneTimePasswords.uri — Methoduri(::HOTP, secret::AbstractString,
account::AbstractString, issuer::AbstractString;
digits::Int=6, counter::Integer=0,
algorithm::Symbol=:SHA1)::StringReturn 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.
OneTimePasswords.uri — Methoduri(::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
)::StringReturn 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.
OneTimePasswords.uri — Methoduri(::TOTP, secret::AbstractString,
account::AbstractString, issuer::AbstractString;
digits::Int=6, period::Period=Second(30))::StringReturn 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.
OneTimePasswords.verify — Methodverify(::HOTP, secret::Union{AbstractString,Vector{UInt8}},
counter::Integer, code::AbstractString; digits::Int=6,
algorithm::Symbol=:SHA1)::BoolReturn 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.
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).
OneTimePasswords.verify — Methodverify(::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)::BoolVerify 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")
truejulia> 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)
trueOneTimePasswords.verify — Methodverify(::TOTP, secret, code::AbstractString;
period::Period=Second(30), allowed_drift::Period=Second(30),
digits::Int=6, time=nothing,
algorithm::Symbol=:SHA1)::BoolReturn true if code is a valid TOTP for secret at time, allowing ±allowed_drift time window.
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.
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))
falsejulia> 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).