module

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

DEFAULT_DIGITS = 6

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.

DEFAULT_PERIOD = 30.seconds

Seconds per code. Thirty is what every authenticator app assumes.

PERMITTED_DIGITS = {6, 7, 8}

Digit counts a client can actually display and a person can actually read back.

Class methods

code(secret : Bytes, counter : Int64, digits : Int32 = DEFAULT_DIGITS, algorithm : Algorithm = Algorithm::SHA1) : String

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.

Source
counter(at : Time, period : Time::Span = DEFAULT_PERIOD) : Int64

The RFC 4226 counter for at: whole steps since the Unix epoch.

Source
match(secret : Bytes, candidate : String, at : Time, period : Time::Span = DEFAULT_PERIOD, digits : Int32 = DEFAULT_DIGITS, algorithm : Algorithm = Algorithm::SHA1, drift : Int32 = 1) : Int64 | Nil

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.

Source
provisioning_uri(secret : Bytes, issuer : String, label : String, period : Time::Span = DEFAULT_PERIOD, digits : Int32 = DEFAULT_DIGITS, algorithm : Algorithm = Algorithm::SHA1) : String

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.

Source

Nested types