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