package data import ( "crypto/hmac" "crypto/rand" // #nosec G505 - SHA1 is used here because TOTP (RFC 6238) specifically uses HMAC-SHA1 // as the default algorithm, and many authenticator apps still use it. // In the future, we should consider supporting stronger algorithms like SHA256 or SHA512. "crypto/sha1" "encoding/base32" "encoding/binary" "fmt" "image/png" "os" "strings" "time" "rsc.io/qr" ) const ( // TOTPTimeStep is the time step in seconds for TOTP. TOTPTimeStep = 30 // TOTPDigits is the number of digits in a TOTP code. TOTPDigits = 6 // TOTPModulo is the modulo value for truncating the TOTP hash. TOTPModulo = 1000000 // TOTPTimeWindow is the number of time steps to check before and after the current time. TOTPTimeWindow = 1 // Constants for TOTP calculation timeBytesLength = 8 dynamicTruncationMask = 0x0F truncationModulusMask = 0x7FFFFFFF ) // GenerateRandomBase32 generates a random base32 encoded string of the specified length. func GenerateRandomBase32(length int) (string, error) { // Generate random bytes randomBytes := make([]byte, length) _, err := rand.Read(randomBytes) if err != nil { return "", err } // Encode to base32 encoder := base32.StdEncoding.WithPadding(base32.NoPadding) encoded := encoder.EncodeToString(randomBytes) // Convert to uppercase and remove any padding return strings.ToUpper(encoded), nil } // ValidateTOTP validates a TOTP code against a secret func ValidateTOTP(secret, code string) bool { // Get current time step currentTime := time.Now().Unix() / TOTPTimeStep // Try the time window (allow for time skew) for i := -TOTPTimeWindow; i <= TOTPTimeWindow; i++ { if calculateTOTP(secret, currentTime+int64(i)) == code { return true } } return false } // calculateTOTP calculates the TOTP code for a given secret and time func calculateTOTP(secret string, timeCounter int64) string { // Decode the secret from base32 encoder := base32.StdEncoding.WithPadding(base32.NoPadding) secretBytes, err := encoder.DecodeString(strings.ToUpper(secret)) if err != nil { return "" } // Convert time counter to bytes (big endian) timeBytes := make([]byte, timeBytesLength) binary.BigEndian.PutUint64(timeBytes, uint64(timeCounter)) // Calculate HMAC-SHA1 h := hmac.New(sha1.New, secretBytes) h.Write(timeBytes) hash := h.Sum(nil) // Dynamic truncation offset := hash[len(hash)-1] & dynamicTruncationMask truncatedHash := binary.BigEndian.Uint32(hash[offset:offset+4]) & truncationModulusMask otp := truncatedHash % TOTPModulo // Format as a 6-digit string with leading zeros return fmt.Sprintf("%0*d", TOTPDigits, otp) } // GenerateTOTPQRCode generates a QR code for a TOTP secret and saves it to a file func GenerateTOTPQRCode(secret, username, issuer, outputPath string) error { // Format the TOTP URI according to the KeyURI format // https://github.com/google/google-authenticator/wiki/Key-Uri-Format uri := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=%d&period=%d", issuer, username, secret, issuer, TOTPDigits, TOTPTimeStep) // Generate QR code code, err := qr.Encode(uri, qr.M) if err != nil { return fmt.Errorf("failed to generate QR code: %w", err) } // Create output file file, err := os.Create(outputPath) if err != nil { return fmt.Errorf("failed to create output file: %w", err) } defer file.Close() // Write QR code as PNG img := code.Image() err = png.Encode(file, img) if err != nil { return fmt.Errorf("failed to write QR code to file: %w", err) } return nil }