Documentation
¶
Overview ¶
Package totp is time-based one-time passwords: RFC 6238, and the factor a policy asks.
secret, _ := totp.ParseSecret("JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP")
if err := totp.Verify(secret, code, totp.Options{}); err != nil { … }
The six digits on a phone are HOTP (RFC 4226) with the counter replaced by the current half-minute. That is the whole idea, and everything else here is consequence:
The code is valid for a WHOLE step -- thirty seconds by default -- so a server that accepts one twice accepts a replay. RFC 6238 5.2 says the verifier MUST NOT accept the same OTP twice, and Verify cannot do that on its own: it holds nothing between calls. Verifier does, and a server should use it.
Clocks differ, so a window of one step either side is allowed by default. Each step of window is a step of somebody else's guessing time; it is a number to set deliberately, not to raise until the complaints stop.
The comparison is constant-time. A six-digit code has a million values and a server that leaks how many leading digits were right has far fewer.
The secret is the credential ¶
Anybody holding it produces every future code. It is not a hash of anything, it does not expire, and a directory that publishes it has published the second factor -- see github.com/go-authn/directory, where that argument is made about the NT hash and applies here word for word.
Index ¶
- Constants
- Variables
- func At(secret []byte, t time.Time, o Options) (string, error)
- func Factor(name string, secret, code []byte, v *Verifier) mfa.Factor
- func FormatSecret(secret []byte) string
- func Generate(secret []byte, step int64, o Options) (string, error)
- func ParseSecret(s string) ([]byte, error)
- func URI(issuer, account string, secret []byte, o Options) string
- func Verify(secret, code []byte, o Options) error
- type Algorithm
- type Options
- type Verifier
Constants ¶
const ( DefaultThrottle = 5 DefaultPause = 15 * time.Minute DefaultLockout = 100 )
The defaults. RFC 4226 §7.3 asks for a throttling parameter "as low as possible, while still ensuring that usability is not significantly impacted" and names none; five is a person mistyping more than they ever do. NIST SP 800-63B §5.2.2 sets the ceiling the lockout sits on.
const MaxWindow = 10
MaxWindow is the widest window accepted: ten steps either side, five minutes at the default period, is a clock wronger than any that should be trusted. ⛔ Without a ceiling, a window of 200,000 steps accepted half of all codes typed at random, and math.MaxInt never returned: the loop over the window overflowed before it could end.
const MinSecret = 16
MinSecret is the shortest secret accepted, in bytes: RFC 4226 R6 says the shared secret MUST be at least 128 bits, and recommends 160.
const NoWindow = -1
NoWindow accepts only the current step. It is a distinct value because 0 means "the default", and a caller asking for no tolerance at all should not be given some.
Variables ¶
var ErrLockedOut = fmt.Errorf("%w; locked out until reset", ErrThrottled)
ErrLockedOut is a name that sent Verifier.Lockout wrong codes in a row. It does not pass with time: Verifier.Forget is the way back, and it belongs to whoever can tell the person from somebody guessing. errors.Is reports it as ErrThrottled too, so a caller that only checks for that refuses it.
var ErrThrottled = errors.New("totp: too many wrong codes in a row")
ErrThrottled is a name that sent too many wrong codes in a row and must wait before another is looked at. The code given with it was not checked, so it says nothing about whether that code was right.
var ErrUsed = fmt.Errorf("totp: that code has already been used")
ErrUsed is a code that was right and has already been accepted. It is distinct from ErrWrongCode because the two mean different things TO A SERVER -- one is a replay, worth noticing -- while a client is told the same thing either way.
var ErrWrongCode = fmt.Errorf("totp: that code is not right")
ErrWrongCode is a code that is not right for any step in the window. It is deliberately the same error for "wrong" and "too old": telling somebody which tells whoever is guessing which.
Functions ¶
func Factor ¶
Factor turns a code somebody typed into a factor a policy can ask.
r, err := mfa.Verify(ctx, mfa.Policy{Count: 2, DistinctKinds: true},
password.Factor(given, want),
totp.Factor("dora", secret, code, verifier),
)
⛔ It is POSSESSION, not knowledge. The person proves they hold the thing the secret was enrolled into -- a phone, a hardware token -- and that is what makes it a second factor next to a password. A policy asking for distinct kinds is relying on this classification, so it is worth saying plainly that a code read aloud over the telephone is possession no longer.
⛔ The Verifier is required. Without one there is nothing to refuse a replay or to limit guessing -- a million codes, tried in well under a second -- and the factor used to fall back to Verify silently when given nil. A nil Verifier is a caller's mistake, and since an mfa.Factor has nowhere to return an error until it is asked, it is reported then, by Verify, for every code.
func FormatSecret ¶
FormatSecret writes a secret the way an authenticator shows it.
func Generate ¶
Generate produces the code for one step. It is what an authenticator does, and what a test needs to produce a code a server should accept.
func ParseSecret ¶
ParseSecret reads a secret in the base32 spelling authenticators use -- upper case, spaces and padding optional, which is how people copy them.
func URI ¶
URI is the otpauth:// URL an authenticator reads from a QR code.
The secret is IN it, which is what enrolment means -- so it is shown to the person enrolling and to nobody else, never logged, and never put in a link something else will follow.
func Verify ¶
Verify checks a code against the current step and the window around it.
⛔ It does not, and cannot, refuse a code it has already accepted: nothing is kept between calls. A code is good for a whole period, so a server that only calls this accepts a replay within the same half-minute -- see Verifier, which is the thing to use in a server.
Types ¶
type Algorithm ¶
type Algorithm int
Algorithm is the hash inside the HMAC.
SHA1 is the default because it is what every authenticator app implements; the others are in RFC 6238 and are read by fewer things. This is a compatibility choice, not a security claim: HMAC-SHA1 is not weakened by the collision attacks that retired SHA1 for signatures.
type Options ¶
type Options struct {
// Digits is how many digits the code has. 0 means 6.
Digits int
// Period is how long one code lasts. 0 means 30 seconds.
Period time.Duration
// Algorithm is the hash inside the HMAC.
Algorithm Algorithm
// Window is how many steps either side of now are accepted, for clocks
// that differ. 0 means 1 -- thirty seconds of slack in each direction.
// Use [NoWindow] to accept only the current step. More than [MaxWindow]
// is refused.
Window int
// Now overrides the clock, for tests and for a server that has a better
// idea of the time than this process does.
Now func() time.Time
}
Options are the parameters an authenticator was enrolled with. The zero value is what every phone assumes: six digits, thirty seconds, SHA-1, and one step of tolerance either way.
type Verifier ¶
type Verifier struct {
Options Options
// Throttle is how many wrong codes in a row a name may send before it
// is made to wait. Zero or less means [DefaultThrottle].
Throttle int
// Pause is how long it waits. Zero or less means [DefaultPause].
Pause time.Duration
// Lockout is how many wrong codes in a row lock a name out until
// [Verifier.Forget]. Zero or less means [DefaultLockout].
Lockout int
// contains filtered or unexported fields
}
A Verifier is Verify plus the things a server needs and a function cannot have: a memory of which codes were already used, and of how many wrong ones each name has sent.
⛔ A code is valid for a whole step. Without this, somebody who watches one being typed -- over a shoulder, in a log, on a wire that was not TLS -- can use it themselves for the rest of that half-minute. RFC 6238 §5.2: the verifier MUST NOT accept a second attempt of an OTP that already succeeded.
⛔ And a code has only a million values. Without a limit, trying them all against one name takes well under a second; RFC 4226 §7.3 says the server needs to detect and stop that, and NIST SP 800-63B §5.2.2 says it SHALL limit consecutive failed attempts to no more than 100. So:
- after Verifier.Throttle wrong codes in a row for a name, that name is refused with ErrThrottled -- without its code being looked at, so even the right one -- until Verifier.Pause has passed;
- after Verifier.Lockout wrong codes in a row, however spread out, it is refused with ErrLockedOut until Verifier.Forget is called for it.
Only a wrong code counts: a replay, a code of the wrong length, a refused secret or parameters that cannot work are refused for their own reasons. A code that is accepted starts the count again. With the defaults, somebody guessing gets 100 tries in about five hours and then none: with the default window of three steps that is a chance of 3 in 10,000 for six digits.
⛔ The memory is per CREDENTIAL -- per secret -- not per name. It was per name, and the name is whatever string the caller passes: a login form that does not canonicalise gave "dora", "Dora", "DORA" ... sixteen counters for one secret, and 1,600 guesses where the lockout said 100. What a guesser attacks is the secret, so that is what the count follows. A replay is refused per secret too, which is what RFC 6238 §5.2 asks: the code accepted was the secret's, under whatever name it was typed. The secret is held only as its SHA-256.
The name is still remembered, beside the credential it last presented, for one purpose: Verifier.Forget takes a name.
The memory is this process's. Two servers behind a load balancer each count their own guesses and neither knows about the other's; a deployment that needs one answer needs a shared store, and this is the interface to put one behind. A credential nobody has tried for a day is forgotten, and so is a name, so a stream of either cannot grow it for ever -- except a credential that is locked out, because forgetting that would be an unlock.
The zero Verifier works, with the default options and limits.
func (*Verifier) Forget ¶
Forget drops what is remembered about the credential a name last presented -- for a person removed from a directory, an administrator unlocking an account after ErrLockedOut, or a test that means to start again.
A name is remembered for a day after it was last tried, refused or not, so a person who was locked out and tried again is found under the name they used.
func (*Verifier) Verify ¶
Verify checks a code for one person and refuses one that was already used, and refuses to look at all while that person's credential is throttled or locked out.
The name is whatever the caller calls people. It does not have to be canonical: what is counted is the secret, under every spelling of the name. It is what Verifier.Forget is later called with.
⛔ The whole check is made under one lock. RFC 4226 §7.3: the limit MUST hold "to prevent attacks based on multiple parallel guessing techniques", and a count read before the code is checked and written after lets every guess sent at the same moment find it below the limit. The price is that one Verifier checks one code at a time: three HMACs, microseconds.