Files
ResolveSpec/pkg/security/totp.go
Hein fdf9e118c5 feat(security): Add two-factor authentication support
* Implement TwoFactorAuthenticator for 2FA login.
* Create DatabaseTwoFactorProvider for PostgreSQL integration.
* Add MemoryTwoFactorProvider for in-memory testing.
* Develop TOTPGenerator for generating and validating codes.
* Include tests for all new functionalities.
* Ensure backup codes are securely hashed and validated.
2026-01-31 22:45:28 +02:00

189 lines
5.4 KiB
Go

package security
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base32"
"encoding/binary"
"fmt"
"hash"
"math"
"net/url"
"strings"
"time"
)
// TwoFactorAuthProvider defines interface for 2FA operations
type TwoFactorAuthProvider interface {
// Generate2FASecret creates a new secret for a user
Generate2FASecret(userID int, issuer, accountName string) (*TwoFactorSecret, error)
// Validate2FACode verifies a TOTP code
Validate2FACode(secret string, code string) (bool, error)
// Enable2FA activates 2FA for a user (store secret in your database)
Enable2FA(userID int, secret string, backupCodes []string) error
// Disable2FA deactivates 2FA for a user
Disable2FA(userID int) error
// Get2FAStatus checks if user has 2FA enabled
Get2FAStatus(userID int) (bool, error)
// Get2FASecret retrieves the user's 2FA secret
Get2FASecret(userID int) (string, error)
// GenerateBackupCodes creates backup codes for 2FA
GenerateBackupCodes(userID int, count int) ([]string, error)
// ValidateBackupCode checks and consumes a backup code
ValidateBackupCode(userID int, code string) (bool, error)
}
// TwoFactorSecret contains 2FA setup information
type TwoFactorSecret struct {
Secret string `json:"secret"` // Base32 encoded secret
QRCodeURL string `json:"qr_code_url"` // URL for QR code generation
BackupCodes []string `json:"backup_codes"` // One-time backup codes
Issuer string `json:"issuer"` // Application name
AccountName string `json:"account_name"` // User identifier (email/username)
}
// TwoFactorConfig holds TOTP configuration
type TwoFactorConfig struct {
Algorithm string // SHA1, SHA256, SHA512
Digits int // Number of digits in code (6 or 8)
Period int // Time step in seconds (default 30)
SkewWindow int // Number of time steps to check before/after (default 1)
}
// DefaultTwoFactorConfig returns standard TOTP configuration
func DefaultTwoFactorConfig() *TwoFactorConfig {
return &TwoFactorConfig{
Algorithm: "SHA1",
Digits: 6,
Period: 30,
SkewWindow: 1,
}
}
// TOTPGenerator handles TOTP code generation and validation
type TOTPGenerator struct {
config *TwoFactorConfig
}
// NewTOTPGenerator creates a new TOTP generator with config
func NewTOTPGenerator(config *TwoFactorConfig) *TOTPGenerator {
if config == nil {
config = DefaultTwoFactorConfig()
}
return &TOTPGenerator{
config: config,
}
}
// GenerateSecret creates a random base32-encoded secret
func (t *TOTPGenerator) GenerateSecret() (string, error) {
secret := make([]byte, 20)
_, err := rand.Read(secret)
if err != nil {
return "", fmt.Errorf("failed to generate random secret: %w", err)
}
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(secret), nil
}
// GenerateQRCodeURL creates a URL for QR code generation
func (t *TOTPGenerator) GenerateQRCodeURL(secret, issuer, accountName string) string {
params := url.Values{}
params.Set("secret", secret)
params.Set("issuer", issuer)
params.Set("algorithm", t.config.Algorithm)
params.Set("digits", fmt.Sprintf("%d", t.config.Digits))
params.Set("period", fmt.Sprintf("%d", t.config.Period))
label := url.PathEscape(fmt.Sprintf("%s:%s", issuer, accountName))
return fmt.Sprintf("otpauth://totp/%s?%s", label, params.Encode())
}
// GenerateCode creates a TOTP code for a given time
func (t *TOTPGenerator) GenerateCode(secret string, timestamp time.Time) (string, error) {
// Decode secret
key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(secret))
if err != nil {
return "", fmt.Errorf("invalid secret: %w", err)
}
// Calculate counter (time steps since Unix epoch)
counter := uint64(timestamp.Unix()) / uint64(t.config.Period)
// Generate HMAC
h := t.getHashFunc()
mac := hmac.New(h, key)
// Convert counter to 8-byte array
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, counter)
mac.Write(buf)
sum := mac.Sum(nil)
// Dynamic truncation
offset := sum[len(sum)-1] & 0x0f
truncated := binary.BigEndian.Uint32(sum[offset:]) & 0x7fffffff
// Generate code with specified digits
code := truncated % uint32(math.Pow10(t.config.Digits))
format := fmt.Sprintf("%%0%dd", t.config.Digits)
return fmt.Sprintf(format, code), nil
}
// ValidateCode checks if a code is valid for the secret
func (t *TOTPGenerator) ValidateCode(secret, code string) (bool, error) {
now := time.Now()
// Check current time and skew window
for i := -t.config.SkewWindow; i <= t.config.SkewWindow; i++ {
timestamp := now.Add(time.Duration(i*t.config.Period) * time.Second)
expected, err := t.GenerateCode(secret, timestamp)
if err != nil {
return false, err
}
if code == expected {
return true, nil
}
}
return false, nil
}
// getHashFunc returns the hash function based on algorithm
func (t *TOTPGenerator) getHashFunc() func() hash.Hash {
switch strings.ToUpper(t.config.Algorithm) {
case "SHA256":
return sha256.New
case "SHA512":
return sha512.New
default:
return sha1.New
}
}
// GenerateBackupCodes creates random backup codes
func GenerateBackupCodes(count int) ([]string, error) {
codes := make([]string, count)
for i := 0; i < count; i++ {
code := make([]byte, 4)
_, err := rand.Read(code)
if err != nil {
return nil, fmt.Errorf("failed to generate backup code: %w", err)
}
codes[i] = fmt.Sprintf("%08X", binary.BigEndian.Uint32(code))
}
return codes, nil
}