mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-02-01 07:24:25 +00:00
* 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.
189 lines
5.4 KiB
Go
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
|
|
}
|