// Package totp implements RFC 6238 time-based one-time passwords, the // built-in second factor for password logins. package totp import ( "crypto/hmac" "crypto/rand" "crypto/sha1" "encoding/base32" "encoding/binary" "fmt" "net/url" "strings" "time" ) const ( digits = 6 step = 30 * time.Second // skew is how many steps either side of now are accepted, covering // clock drift and a user typing slowly. skew = 1 ) // NewSecret returns a fresh base32 secret with no padding, as authenticator // apps expect. func NewSecret() string { b := make([]byte, 20) if _, err := rand.Read(b); err != nil { panic("totp: " + err.Error()) } return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b) } // URI is the otpauth:// URL an authenticator app can scan. func URI(issuer, account, secret string) string { label := url.PathEscape(issuer + ":" + account) q := url.Values{"secret": {secret}, "issuer": {issuer}, "algorithm": {"SHA1"}, "digits": {fmt.Sprint(digits)}, "period": {"30"}} return "otpauth://totp/" + label + "?" + q.Encode() } // Code is the password for secret at time t. func Code(secret string, t time.Time) (string, error) { key, err := decode(secret) if err != nil { return "", err } return code(key, uint64(t.Unix())/uint64(step.Seconds())), nil } // Check reports whether given is a valid password for secret at time t, // allowing one step of drift either way. func Check(secret, given string, t time.Time) bool { _, ok := Match(secret, given, t) return ok } // Match is Check that also says which time step the code belongs to. A // caller that remembers the last step it accepted, and refuses that one and // any before it, makes each code good for one use only. func Match(secret, given string, t time.Time) (int64, bool) { key, err := decode(secret) if err != nil { return 0, false } given = strings.TrimSpace(given) if len(given) != digits { return 0, false } ctr := uint64(t.Unix()) / uint64(step.Seconds()) for d := -skew; d <= skew; d++ { if hmac.Equal([]byte(code(key, ctr+uint64(d))), []byte(given)) { return int64(ctr) + int64(d), true } } return 0, false } func decode(secret string) ([]byte, error) { s := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(secret), " ", "")) return base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s) } func code(key []byte, counter uint64) string { var buf [8]byte binary.BigEndian.PutUint64(buf[:], counter) mac := hmac.New(sha1.New, key) mac.Write(buf[:]) sum := mac.Sum(nil) off := sum[len(sum)-1] & 0x0f trunc := binary.BigEndian.Uint32(sum[off:off+4]) & 0x7fffffff mod := uint32(1) for i := 0; i < digits; i++ { mod *= 10 } return fmt.Sprintf("%0*d", digits, trunc%mod) }