Cryptographically secure numeric one-time-password primitive for the
mix skua.gen.auth OTP flows.
The generated auth code calls into this so the security-critical bits live in one tested place rather than copy-pasted (and possibly mangled) into every app:
generate/1— an unbiased N-digit numeric code via CSPRNG rejection sampling, returned as a fixed-width string with leading zeros preserved.hash/1— SHA-256 of the code string. Store the hash, never the code.normalize/2— coerce a submitted code to a canonical digits-only string and validate its length. Pins the wire format: rejects short/long or integer-coerced input rather than zero-padding it (which would otherwise collapse the code space or silently fail leading-zero codes).valid?/3— constant-time comparison of a submitted code against a stored hash, after normalization.
Security properties (each is covered by a test)
- No modulo bias. Codes are drawn with
:crypto.strong_rand_bytes/1and rejection sampling, so every value in0..(10^digits - 1)is equally likely. - Leading zeros are significant. A 6-digit code is always a 6-character
string;
"004217"hashes and compares as"004217", never4217. - Constant-time compare. Verification uses
Plug.Crypto.secure_compare/2to avoid timing leaks. - Configurable length. 6 digits by default; 8 is recommended for
high-value deployments (
generate(8)), with the same guarantees.
Summary
Functions
Generate an unbiased numeric code of digits length (default 6),
zero-padded to a fixed-width string.
SHA-256 of a code string. Store this; never persist the plaintext code.
Normalize a submitted code to a canonical digits-only string of exactly
digits length. Returns {:ok, code} or :error.
Constant-time check of a submitted code against a stored SHA-256 hash. The
submitted code is normalized to digits first; malformed input returns false.
Types
Functions
Generate an unbiased numeric code of digits length (default 6),
zero-padded to a fixed-width string.
SHA-256 of a code string. Store this; never persist the plaintext code.
Normalize a submitted code to a canonical digits-only string of exactly
digits length. Returns {:ok, code} or :error.
Never zero-pads or integer-coerces: "4217" for a 6-digit flow is rejected,
not turned into "004217", and 4217 (integer) is rejected too.
Constant-time check of a submitted code against a stored SHA-256 hash. The
submitted code is normalized to digits first; malformed input returns false.