| package common |
|
|
| import ( |
| "crypto/rand" |
| "fmt" |
| "os" |
| "strconv" |
| "strings" |
|
|
| "github.com/pquerna/otp" |
| "github.com/pquerna/otp/totp" |
| ) |
|
|
| const ( |
| |
| BackupCodeLength = 8 |
| BackupCodeCount = 4 |
|
|
| |
| MaxFailAttempts = 5 |
| LockoutDuration = 300 |
| ) |
|
|
| |
| func GenerateTOTPSecret(accountName string) (*otp.Key, error) { |
| issuer := Get2FAIssuer() |
| return totp.Generate(totp.GenerateOpts{ |
| Issuer: issuer, |
| AccountName: accountName, |
| Period: 30, |
| Digits: otp.DigitsSix, |
| Algorithm: otp.AlgorithmSHA1, |
| }) |
| } |
|
|
| |
| func ValidateTOTPCode(secret, code string) bool { |
| |
| cleanCode := strings.ReplaceAll(code, " ", "") |
| if len(cleanCode) != 6 { |
| return false |
| } |
|
|
| |
| return totp.Validate(cleanCode, secret) |
| } |
|
|
| |
| func GenerateBackupCodes() ([]string, error) { |
| codes := make([]string, BackupCodeCount) |
|
|
| for i := 0; i < BackupCodeCount; i++ { |
| code, err := generateRandomBackupCode() |
| if err != nil { |
| return nil, err |
| } |
| codes[i] = code |
| } |
|
|
| return codes, nil |
| } |
|
|
| |
| func generateRandomBackupCode() (string, error) { |
| const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" |
| code := make([]byte, BackupCodeLength) |
|
|
| for i := range code { |
| randomBytes := make([]byte, 1) |
| _, err := rand.Read(randomBytes) |
| if err != nil { |
| return "", err |
| } |
| code[i] = charset[int(randomBytes[0])%len(charset)] |
| } |
|
|
| |
| return fmt.Sprintf("%s-%s", string(code[:4]), string(code[4:])), nil |
| } |
|
|
| |
| func ValidateBackupCode(code string) bool { |
| |
| cleanCode := strings.ToUpper(strings.ReplaceAll(code, "-", "")) |
| if len(cleanCode) != BackupCodeLength { |
| return false |
| } |
|
|
| |
| for _, char := range cleanCode { |
| if !((char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { |
| return false |
| } |
| } |
|
|
| return true |
| } |
|
|
| |
| func NormalizeBackupCode(code string) string { |
| cleanCode := strings.ToUpper(strings.ReplaceAll(code, "-", "")) |
| if len(cleanCode) == BackupCodeLength { |
| return fmt.Sprintf("%s-%s", cleanCode[:4], cleanCode[4:]) |
| } |
| return code |
| } |
|
|
| |
| func HashBackupCode(code string) (string, error) { |
| normalizedCode := NormalizeBackupCode(code) |
| return Password2Hash(normalizedCode) |
| } |
|
|
| |
| func Get2FAIssuer() string { |
| return SystemName |
| } |
|
|
| |
| func getEnvOrDefault(key, defaultValue string) string { |
| if value, exists := os.LookupEnv(key); exists { |
| return value |
| } |
| return defaultValue |
| } |
|
|
| |
| func ValidateNumericCode(code string) (string, error) { |
| |
| code = strings.ReplaceAll(code, " ", "") |
|
|
| if len(code) != 6 { |
| return "", fmt.Errorf("验证码必须是6位数字") |
| } |
|
|
| |
| if _, err := strconv.Atoi(code); err != nil { |
| return "", fmt.Errorf("验证码只能包含数字") |
| } |
|
|
| return code, nil |
| } |
|
|
| |
| func GenerateQRCodeData(secret, username string) string { |
| issuer := Get2FAIssuer() |
| accountName := fmt.Sprintf("%s (%s)", username, issuer) |
| return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&digits=6&period=30", |
| issuer, accountName, secret, issuer) |
| } |
|
|