Help us shape the future of Go! Take the 2026 Go Developer Survey

totp

package module
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: BSD-3-Clause Imports: 16 Imported by: 0

README

totp

Go Reference License CI

Time-based one-time passwords (RFC 6238), and the factor a policy asks. Pure Go, CGO_ENABLED=0, no dependencies but go-authn/mfa.

secret, _ := totp.ParseSecret("JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP")

// knows is yours: whatever checks the password, as an mfa.Factor whose Kind is
// mfa.Knowledge. This module provides the other half.
r, err := mfa.Verify(ctx, mfa.Policy{Count: 2, DistinctKinds: true},
    knows,                                         // something they know
    totp.Factor("dora", secret, code, verifier),   // something they have
)

The *Verifier is required. Factor with a nil one used to fall back to the stateless Verify — no replay refusal, no throttle, no lockout: an audit found the code after 236,089 wrong guesses in a quarter of a second, then replayed it three times. Now the factor refuses every code, saying a Verifier is missing; it is not reported as unavailable, so a policy cannot pass on the other factors and hide the mistake.

Codes are six digits, every 30 seconds, HMAC-SHA1, one step either side, unless Options says otherwise: Digits, Period, Algorithm (SHA1, SHA256, SHA512) and Window (NoWindow for none: 0 means the default, at most MaxWindow = 10). A Verifier carries its own Options, and FormatSecret writes a secret the way ParseSecret reads it.

The six digits on a phone are HOTP (RFC 4226) with the counter replaced by the current half-minute. That is the whole idea; everything below is consequence.

A code is valid for a whole step, so a server must remember it

RFC 6238 §5.2: the verifier MUST NOT accept a second attempt of an OTP that already succeeded. Verify cannot do that — it holds nothing between calls — so a server uses a Verifier:

v := &totp.Verifier{}
err := v.Verify("dora", secret, code)   // totp.ErrUsed the second time

Every step up to and including the last accepted one is refused, not just the same one: a code from thirty seconds ago is still inside the default window, and accepting it is the same replay with one more step of patience.

The memory is this process's. Two servers behind a load balancer each refuse their own replays and neither knows about the other's — a deployment that needs one answer needs a shared store, and Verifier is the seam to put it behind.

The window is somebody's guessing time

One step either side by default, because clocks differ. NoWindow accepts only the current step — a distinct constant, because 0 means the default and a caller asking for no tolerance should not be handed some.

At most MaxWindow = 10 steps either side; a wider one is refused as a parameter. Without that ceiling Window: 200000 accepted half of all codes typed at random, and math.MaxInt never returned — the loop over the window overflowed before it could end.

Every step in the window is tried and the loop does not stop early: returning as soon as one matches makes the time taken depend on which step it was. The comparison itself 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.

Guessing is limited per secret

A six-digit code has a million values; without a limit, trying them in order against one name got in after 292,211 guesses in 372 ms in an audit run. RFC 4226 §7.3 says the server needs to detect and stop that, and that the limit MUST hold across sessions and against parallel guessing. NIST SP 800-63B §5.2.2 says a verifier SHALL limit consecutive failed attempts on one account to no more than 100. A Verifier does both, counting per credential — per secret — and not per name:

field default what it does
Throttle DefaultThrottle = 5 after this many wrong codes in a row, the secret gets ErrThrottled…
Pause DefaultPause = 15 min …for this long; the code sent meanwhile is not looked at, so not even the right one passes
Lockout DefaultLockout = 100 after this many wrong codes in a row, however spread out, the secret gets ErrLockedOut until Forget

RFC 4226 asks for a throttle "as low as possible, while still ensuring that usability is not significantly impacted" and names no number; five is more mistypes in a row than a person makes. The lockout is NIST's ceiling. With the defaults a guesser gets 100 tries in about five hours and then none — with the default window of three steps, a chance of 3 in 10,000 against six digits. errors.Is(err, totp.ErrThrottled) is true for both refusals, so a caller that checks only that refuses both.

Only a wrong code counts. A replay (ErrUsed), a code of the wrong length and a refused secret or parameter are refused for their own reasons and do not bring a pause nearer. An accepted code starts the count again. The whole check is made under one lock, so guesses sent at the same moment are counted one by one rather than each finding the count below the limit.

⛔ 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: 1,600 wrong guesses where the lockout said 100. What a guesser attacks is the secret, so the count follows it, under every spelling. A replay is refused per secret too — the code accepted was the secret's, whatever name it was typed under. The secret is held only as its SHA-256. The name is still remembered beside the credential it last presented, refused or not, because Forget takes a name: Forget("dora") unlocks the secret dora last tried.

The memory is bounded: a credential or a name nobody has tried for a day is forgotten when the maps have doubled since they were last swept. A locked-out credential is never forgotten that way — that would be an unlock nobody asked for. And the limit is a lockout someone else can trigger: whoever knows a name can lock it. That is the trade-off both documents accept; Forget is the way back, and it belongs to whoever can tell the person from somebody guessing.

Parameters that cannot be right are refused

  • The secret is at least 128 bits (MinSecret = 16 bytes), RFC 4226 R6, in Generate, At, Verify and Verifier.Verify. ParseSecret still reads a shorter one — it is a parser — and nothing will make a code from it.
  • The period is a whole number of seconds, at least one: RFC 6238 counts it in seconds, and half a second used to divide by zero.
  • 6 to 10 digits, RFC 4226 §5.3, with the reduction done in 64 bits: 10^10 does not fit in 32.

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 — the same argument go-authn/directory makes about the NT hash, word for word.

So: no error message here quotes a code or a secret, and URI — the otpauth:// string an authenticator reads from a QR code — carries the secret by construction and is shown to the person enrolling and to nobody else.

Verified against things that are not this package

  • RFC 6238 Appendix B, all six times and all three algorithms. The seeds are the ASCII digits repeated to the hash's key length (20, 32, 64 bytes) — the RFC's table is famously ambiguous about that, so the reading was measured: an independent implementation produces the RFC's published codes from it.
  • RFC 4226 Appendix D, counters 0 to 9: the six-digit codes, and the "truncated" column, which is the ten-digit code because 2^31 < 10^10.
  • 200 random cases against pyotp — secrets of 23 different lengths from 16 bytes, 6 to 10 digits, four periods, all three algorithms. The published vectors pin the arithmetic and leave the edges alone.
  • Every digit count, 1000 steps per algorithm, against RFC 4226 §5.3 computed again in the test with the reduction in arbitrary precision.
  • Both judges were sabotage-checked: masking 0xffffffff instead of 0x7fffffff in the dynamic truncation fails 95 of 206 assertions, and reading the offset from the first byte instead of the last fails 196.

Licence

BSD-3-Clause.

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

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

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

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

View Source
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

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

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

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

View Source
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 At

func At(secret []byte, t time.Time, o Options) (string, error)

At produces the code for a time.

func Factor

func Factor(name string, secret, code []byte, v *Verifier) mfa.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

func FormatSecret(secret []byte) string

FormatSecret writes a secret the way an authenticator shows it.

func Generate

func Generate(secret []byte, step int64, o Options) (string, error)

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

func ParseSecret(s string) ([]byte, error)

ParseSecret reads a secret in the base32 spelling authenticators use -- upper case, spaces and padding optional, which is how people copy them.

func URI

func URI(issuer, account string, secret []byte, o Options) string

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

func Verify(secret, code []byte, o Options) error

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.

const (
	SHA1 Algorithm = iota
	SHA256
	SHA512
)

The hashes RFC 6238 names.

func (Algorithm) String

func (a Algorithm) String() string

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.

func (Options) Step

func (o Options) Step(t time.Time) int64

Step is the counter a time falls in: RFC 6238's T.

For a period Generate and Verify refuse -- under one second -- it counts in seconds rather than dividing by zero; no code is ever made from that step.

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:

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

func (v *Verifier) Forget(name string)

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

func (v *Verifier) Verify(name string, secret, code []byte) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL