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
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.
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.
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.
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.
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.
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.