Starting over.
This commit is contained in:
125
data/totp.go
125
data/totp.go
@@ -1,125 +0,0 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user