class

KemalIdentity::Testing::FastTestHasher

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

A hasher fast enough to run in every login spec.

Real bcrypt at cost 12 is tens of milliseconds per verification, so a suite that used it everywhere would take minutes and nobody would run it on save. This satisfies the same Hasher contract — the contract spec is what stops it drifting into false confidence — and is unreachable from a production build because nothing in kemal_identity requires kemal_identity/testing.

Not a password hasher. A single SHA-256 pass is exactly what a password hasher must not be: it is fast, which is the property that makes offline cracking cheap. The rounds parameter stands in for bcrypt's cost so needs_rehash? has something to compare, and buys no real work.

Constants

MAX_SECRET_BYTESIZE = 71

Matches bcrypt's usable limit so specs written against the double keep meaning something against the real hasher.

SCHEME = "test"

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