package internals // TOTP implementation per RFC 6238 (TOTP) and RFC 4226 (HOTP). // No external dependencies — uses only crypto/hmac, crypto/sha1, encoding/base32. import ( "crypto/hmac" "crypto/rand" "crypto/sha1" "encoding/base32" "encoding/binary" "fmt" "math" "net/url" "strings" "time" ) const ( totpDigits = 6 totpStep = 30 // seconds per time window totpWindow = 1 // ±1 step tolerance for clock drift (~30 s either side) ) // newTOTPSecret generates a random 160-bit (20-byte) base32 secret. // Compatible with Google Authenticator, Aegis, Authy, etc. func newTOTPSecret() string { b := make([]byte, 20) rand.Read(b) //nolint:errcheck return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b) } // decodeTOTPSecret decodes a base32 secret; case-insensitive, padding optional. func decodeTOTPSecret(secret string) ([]byte, error) { s := strings.ToUpper(strings.TrimSpace(secret)) if pad := len(s) % 8; pad != 0 { s += strings.Repeat("=", 8-pad) } return base32.StdEncoding.DecodeString(s) } // hotpAt computes one HOTP value for the given key and counter (RFC 4226 §5). func hotpAt(key []byte, counter uint64) string { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter) mac := hmac.New(sha1.New, key) mac.Write(buf) //nolint:errcheck h := mac.Sum(nil) // Dynamic truncation offset := h[len(h)-1] & 0x0f code := (uint32(h[offset])&0x7f)<<24 | uint32(h[offset+1])<<16 | uint32(h[offset+2])<<8 | uint32(h[offset+3]) code %= uint32(math.Pow10(totpDigits)) return fmt.Sprintf("%0*d", totpDigits, code) } // validateTOTP returns true if code matches the current window ± totpWindow steps. func validateTOTP(secret, code string) bool { if len(code) != totpDigits { return false } for _, c := range code { if c < '0' || c > '9' { return false } } key, err := decodeTOTPSecret(secret) if err != nil { return false } step := uint64(time.Now().Unix()) / totpStep for d := -totpWindow; d <= totpWindow; d++ { if hotpAt(key, uint64(int64(step)+int64(d))) == code { return true } } return false } // totpOtpauthURL returns the otpauth:// URI used to provision authenticator apps. func totpOtpauthURL(secret, username, issuer string) string { return fmt.Sprintf( "otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=%d&period=%d", url.PathEscape(issuer), url.PathEscape(username), secret, url.QueryEscape(issuer), totpDigits, totpStep, ) }