Digestif. Hasher behaviour
(digestif v0.4.1)
Copy Markdown
Behaviour and migration dispatcher for password hashers.
A hasher is configured as a {module, options} tuple. The module performs
one algorithm; this dispatcher identifies the self-describing algorithm in
a stored hash and routes verification only to the configured primary or
legacy hashers.
Hashers may expose a rehash predicate, configuration validation, aliases for compatible stored prefixes, and a password byte limit. The bundled adapters also perform a small resource preflight before invoking their cryptographic backend.
Use the simpler Digestif facade in ordinary applications. This module is
the extension and integration boundary for authentication libraries and
custom hashers.
Summary
Callbacks
Returns the self-describing algorithm identifier this hasher emits and
dispatches on, e.g. "argon2id".
Returns further stored identifiers this hasher also verifies, e.g. bcrypt's
"2a" and "2y" for a "2b" hasher.
Hashes value under options, returning {:ok, encoded_hash}.
Returns whether encoded_hash is weaker than options and should be
replaced.
Does the same work as verify/3 against a dummy hash, then returns false.
Returns the number of leading password bytes this algorithm distinguishes.
Returns :ok for a valid options list, or raises to reject it.
Returns whether value matches encoded_hash under options.
Functions
Returns whether a verified hash should be replaced by the primary hasher.
Returns a hasher's declared password byte limit, or nil when unlimited.
Validates one hasher tuple and its options.
Validates a primary hasher and its legacy migration set.
Verifies a value against the hasher selected by the stored algorithm.
Types
Callbacks
@callback algorithm() :: String.t()
Returns the self-describing algorithm identifier this hasher emits and
dispatches on, e.g. "argon2id".
It must be the identifier segment of every hash hash/2 mints, and it must
be unique across a configured set — dispatch selects a hasher by this value,
never by list order. A hasher listed under :legacy_hashers must implement
it; a primary-only hasher that never needs to be distinguished from others
may omit it and receive every unrecognised encoding.
@callback algorithm_aliases() :: [String.t()]
Returns further stored identifiers this hasher also verifies, e.g. bcrypt's
"2a" and "2y" for a "2b" hasher.
Aliases share algorithm/0's uniqueness rule: within one configured set,
no two hashers may claim the same identifier or alias, so dispatch stays
unambiguous.
Hashes value under options, returning {:ok, encoded_hash}.
The encoded hash must be self-describing: later verification routes on the
algorithm identifier the hash carries, so what hash/2 emits and what
algorithm/0 returns have to agree. Raise on invalid options rather than
mint a hash the same configuration cannot verify.
Returns whether encoded_hash is weaker than options and should be
replaced.
Consulted only for the primary hasher, and only after a successful verify;
a legacy hash always needs rehashing without asking. The dispatcher treats a
raise or a non-boolean result as false, but that is a safety net, not a
contract to lean on — decide deliberately.
Does the same work as verify/3 against a dummy hash, then returns false.
This is the timing defence for a caller who names no stored hash — an
unknown user. "No such account" and "wrong password" must take the same
time, or the difference enumerates accounts. Returning anything truthy here
would authenticate every such caller, so the return is false, always.
@callback password_byte_limit() :: pos_integer()
Returns the number of leading password bytes this algorithm distinguishes.
Implement it only for an algorithm that truncates — bcrypt at 72 bytes. A host that makes such a hasher primary must cap accepted passwords at this limit; otherwise two distinct passwords sharing a prefix verify interchangeably. Omitting the callback declares no limit.
@callback validate_options!(options :: keyword()) :: :ok
Returns :ok for a valid options list, or raises to reject it.
Runs at configuration time, before any password is hashed, so a
misconfiguration fails at startup rather than at the first login. This is the
boundary check that lets hash/2 and verify/3 trust the options they
are given.
@callback verify(value :: String.t(), encoded_hash :: String.t(), options :: keyword()) :: boolean()
Returns whether value matches encoded_hash under options.
The dispatcher sends an unrecognised algorithm identifier to the primary
hasher, so a primary implementation is handed foreign and malformed
encodings and must fail closed as false — after work indistinguishable in
time from a real comparison, never a fast structural reject that leaks, by
latency, whether the hash was even one of its own. Compare digests with
:crypto.hash_equals/2, not ==.
Functions
Returns whether a verified hash should be replaced by the primary hasher.
A hash verified by a legacy hasher always needs rehashing. For the primary
hasher, its optional needs_rehash?/2 callback decides.
@spec password_byte_limit(t()) :: pos_integer() | nil
Returns a hasher's declared password byte limit, or nil when unlimited.
@spec validate!(t()) :: :ok
Validates one hasher tuple and its options.
Validates a primary hasher and its legacy migration set.
Legacy hashers must declare self-describing algorithm identifiers. All identifiers and aliases must be unique, so dispatch cannot depend on list order.
Verifies a value against the hasher selected by the stored algorithm.
Only the primary hasher and explicitly listed legacy hashers are eligible. Unknown or malformed algorithm identifiers fall back to the primary hasher, which must fail closed for foreign encodings.