// Package totp implements TOTP (RFC 6238, built on HOTP RFC 4226) — // hand-rolled on stdlib crypto/hmac + crypto/sha1 + encoding/base32, no // third-party OTP library. Correctness is checked against RFC 6238's own // published test vectors (Appendix B) in the test suite, not just "it // produces a 6-digit number." package totp import ( "crypto/hmac" "crypto/rand" "crypto/sha1" "encoding/base32" "fmt" "math" "net/url" "strconv" "strings" "time" ) const ( period = 30 // seconds per RFC 6238's recommended default digits = 6 ) // GenerateSecret creates a new random 20-byte (160-bit) secret, base32 // encoded — the standard size real authenticator apps (Google Authenticator, // Authy, etc.) expect. func GenerateSecret() (string, error) { b := make([]byte, 20) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("generating TOTP secret: %w", err) } return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b), nil } // Generate computes the TOTP code for secret at the given time — exported // primarily so the test suite can check RFC 6238's published vectors, which // specify exact codes for exact timestamps. func Generate(secret string, at time.Time) (string, error) { key, err := decodeSecret(secret) if err != nil { return "", err } counter := uint64(at.Unix() / period) return hotp(key, counter), nil } // Validate checks code against the current time step and, per common TOTP // practice, the one step before and after (±30s) to tolerate minor clock // drift between server and authenticator app. func Validate(secret, code string) (bool, error) { key, err := decodeSecret(secret) if err != nil { return false, err } code = strings.TrimSpace(code) now := time.Now().UTC() counter := uint64(now.Unix() / period) for _, skew := range []int64{0, -1, 1} { c := hotp(key, uint64(int64(counter)+skew)) if c == code { return true, nil } } return false, nil } // hotp implements RFC 4226 HOTP — the counter-based primitive TOTP wraps. func hotp(key []byte, counter uint64) string { msg := make([]byte, 8) for i := 7; i >= 0; i-- { msg[i] = byte(counter & 0xff) counter >>= 8 } mac := hmac.New(sha1.New, key) mac.Write(msg) sum := mac.Sum(nil) offset := sum[len(sum)-1] & 0x0f binCode := (uint32(sum[offset])&0x7f)<<24 | (uint32(sum[offset+1])&0xff)<<16 | (uint32(sum[offset+2])&0xff)<<8 | (uint32(sum[offset+3]) & 0xff) mod := uint32(math.Pow10(digits)) return fmt.Sprintf("%0*d", digits, binCode%mod) } func decodeSecret(secret string) ([]byte, error) { secret = strings.ToUpper(strings.TrimSpace(secret)) secret = strings.ReplaceAll(secret, " ", "") key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(secret) if err != nil { return nil, fmt.Errorf("decoding TOTP secret: %w", err) } return key, nil } // ProvisioningURI builds the otpauth:// URI real authenticator apps use to // set up an account — either scanned as a QR code (QR rendering itself is // deliberately not implemented here, see package doc note below) or // manually entered, since every mainstream authenticator app supports // typing in the secret directly as a fallback to scanning. // // Note: this package does not generate a QR code image. Real QR encoding // (Reed-Solomon error correction, matrix placement) is a substantial // sub-project of its own with little shared surface with TOTP itself — // deferred rather than half-implemented. The webmail MFA setup page // displays this URI as both a copyable string and (optionally, via a // client-side QR library the frontend can add later) a scannable code. func ProvisioningURI(secret, accountEmail, issuer string) string { v := url.Values{} v.Set("secret", secret) v.Set("issuer", issuer) v.Set("algorithm", "SHA1") v.Set("digits", strconv.Itoa(digits)) v.Set("period", strconv.Itoa(period)) label := url.PathEscape(issuer) + ":" + url.PathEscape(accountEmail) return fmt.Sprintf("otpauth://totp/%s?%s", label, v.Encode()) }