KemalIdentity::MFA::TOTP
Time-based one-time passwords, RFC 6238.
The whole algorithm is HOTP (RFC 4226) with a counter derived from the clock: divide the seconds since the epoch by a step, HMAC the counter under a shared secret, and truncate the result to a few digits. That is all a second factor is — proof that the holder has the secret, sampled at a moment both sides can agree on.
This module decides nothing
It computes and compares codes. It does not know which account a secret belongs to, whether
a code was already used, or how many attempts have been made — those are MFA::Service's
job, and they are the parts that actually make TOTP safe. A six-digit code is one of a
million, and an unlimited number of guesses against a window that never closes is not a
second factor at all.
Constants
Digits per code. Six is the universal default; eight is permitted by RFC 6238 and supported here, and anything else is refused rather than silently truncated.
Seconds per code. Thirty is what every authenticator app assumes.
Digit counts a client can actually display and a person can actually read back.
Class methods
The code for one counter value.
Returned as a zero-padded string rather than an integer, because 042311 and 42311
are the same number and only one of them is the code.
The RFC 4226 counter for at: whole steps since the Unix epoch.
The counter a candidate matches within drift steps either side of at, or nil.
Returning the counter rather than a boolean is what lets the caller refuse a code it has already seen. Without that, every code stays valid for its whole window and a shoulder- surfed six digits can be replayed for the next thirty seconds — which is exactly the window an attacker who watched someone type it is working in.
drift exists because the two clocks are never identical, and it is a cost: each step of
tolerance multiplies the number of codes valid at any moment. One step either side is the
usual compromise and the default.
The otpauth:// URI an authenticator app scans.
issuer names the service and label names the account within it — usually the login,
since it is what the person will recognise in a list of six-digit codes. Both appear in
the path and issuer again in the query, which is redundant and is what the apps
actually parse.
The result contains the secret. It is a credential: render it to a QR code and never log it, put it in a URL that leaves the machine, or store it beside the ciphertext it came from.