class

KemalIdentity::Testing::CountingHasher

Inherits KemalIdentity::Passwords::Hasher < Reference < Object

Counts what the hasher underneath it was asked to do.

The timing equalisation in MigratingHasher is a cost, and a spec that asserted it by measuring elapsed time would be a spec that flakes on a loaded runner. Counting the work instead is deterministic and says the same thing: the failure path did the same amount of hashing as a current-scheme login.

Constructors

Instance methods

dummy_digest

A digest that no input verifies against, costing what a real verification costs.

This closes the enumeration-timing oracle. If an unknown login returns before doing any hashing work, the response comes back a hundred milliseconds early and the attacker has a reliable account oracle no matter how identical the response body is:

account = accounts.find_by_login(normalized, tenant_id)
digest = account.try(&.password_digest) || hasher.dummy_digest
ok = hasher.verify(submitted, digest)
return Failed.new(FailureReason::InvalidCredential) if account.nil? || !ok

Computed once, when the hasher is built, so it costs nothing per request.

Source
hash_secret(secret : KemalIdentity::Secret) : String

Digests secret at the current parameters.

Raises ArgumentError if secret is empty or longer than #max_secret_bytesize. The message carries the length and never the secret.

Source
hashes
Source
max_secret_bytesize

The largest secret this algorithm can represent, in bytes — not characters. A multi-byte character costs more than one byte of the budget, so a limit measured in characters would be wrong for exactly the users least likely to be testing it.

Policy reads this to reject an over-long secret with a useful message before #hash_secret raises on it.

Source
needs_rehash?(digest : String) : Bool

Whether digest was produced at parameters weaker than the current ones, or by another scheme entirely.

This is what makes lazy rehashing work: a successful login at an outdated cost silently rehashes at the current one, so old digests disappear as people sign in and nobody is forced through a password reset (docs/06-roadmap.md, migration step 2). A digest this hasher cannot parse counts as needing a rehash — that is precisely the legacy digest the migration is trying to retire.

Source
reset_counts
Source
scheme

Identifies the algorithm, and is stored alongside the digest in auth_accounts.password_scheme so #needs_rehash? can tell a foreign digest from one of ours.

Source
verifications
Source
verify(secret : KemalIdentity::Secret, digest : String) : Bool

Whether secret produced digest.

Returns false — never raises, never truncates — for a secret the algorithm cannot represent, and for a digest this hasher cannot parse.

Source