totp

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: BSD-3-Clause Imports: 15 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("JBSWY3DPEHPK3PXP")

r, err := mfa.Verify(ctx, mfa.Policy{Count: 2, DistinctKinds: true},
    password.Factor(given),                        // something they know
    totp.Factor("dora", secret, code, verifier),   // something they have
)

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.

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.

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.
  • 200 random cases against pyotp — secrets of 23 different lengths, 6 to 9 digits, four periods, all three algorithms. The published vectors pin the arithmetic and leave the edges alone.
  • 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("JBSWY3DPEHPK3PXP")
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 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 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.

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

type Verifier

type Verifier struct {
	Options Options
	// contains filtered or unexported fields
}

A Verifier is Verify plus the thing a server needs and a function cannot have: a memory of which codes were already used.

⛔ 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.

The memory is per NAME, and it 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 this is the interface to put one behind.

The zero Verifier works, with the default options.

func (*Verifier) Forget

func (v *Verifier) Forget(name string)

Forget drops what is remembered about a name -- for a person removed from a directory, or a test that means to start again.

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.

The name is whatever the caller calls people, and it only has to be stable: it is a map key here and nothing else.

Jump to

Keyboard shortcuts

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