veza/veza-backend-api/internal/validators/password_validator.go
senke 616a0ebc9c [BE-SEC-006] security: Implement comprehensive password strength validation
- Enhanced PasswordValidator with additional security checks:
  * Maximum length validation (128 characters)
  * Common password detection (password, 123456, qwerty, etc.)
  * Repetitive pattern detection (aaaa, 1111, etc.)
  * Sequential pattern detection (1234, abcd, qwerty, etc.)
- Added ValidatePasswordChange method to ensure new password is
  sufficiently different from old password (similarity check)
- Updated PasswordService to use enhanced validator consistently
- Replaced utils.ValidatePasswordStrength with validators.PasswordValidator
- All password operations now use the same comprehensive validation rules

Phase: PHASE-4
Priority: P1
Progress: 8/267 (3.0%)
2025-12-24 12:08:03 +01:00

237 lines
5.9 KiB
Go

package validators
import (
"regexp"
"strings"
)
var (
hasUpper = regexp.MustCompile(`[A-Z]`)
hasLower = regexp.MustCompile(`[a-z]`)
hasNumber = regexp.MustCompile(`[0-9]`)
hasSpecial = regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`)
// Common weak passwords list
commonPasswords = []string{
"password", "12345678", "123456789", "1234567890",
"qwerty", "abc123", "monkey", "1234567", "letmein",
"trustno1", "dragon", "baseball", "iloveyou", "master",
"sunshine", "ashley", "bailey", "passw0rd", "shadow",
"123123", "654321", "superman", "qazwsx", "michael",
"football", "welcome", "jesus", "ninja", "mustang",
"password1", "admin", "1234", "12345", "123456",
}
)
// PasswordValidator valide la force d'un mot de passe
type PasswordValidator struct {
MinLength int
}
// NewPasswordValidator crée une nouvelle instance de PasswordValidator
func NewPasswordValidator() *PasswordValidator {
return &PasswordValidator{MinLength: 12}
}
// PasswordStrength représente le résultat de la validation d'un mot de passe
type PasswordStrength struct {
Valid bool
Score int
Details []string
}
// Validate valide la force d'un mot de passe selon les règles définies
// BE-SEC-006: Implement comprehensive password strength validation
func (v *PasswordValidator) Validate(password string) (PasswordStrength, error) {
strength := PasswordStrength{
Valid: true,
Details: []string{},
}
// Length check
if len(password) < v.MinLength {
strength.Valid = false
strength.Details = append(strength.Details,
"Password must be at least 12 characters long")
return strength, nil
}
// Maximum length check (prevent DoS)
if len(password) > 128 {
strength.Valid = false
strength.Details = append(strength.Details,
"Password must be less than 128 characters")
return strength, nil
}
// Check for common weak passwords
passwordLower := strings.ToLower(password)
for _, common := range commonPasswords {
if strings.Contains(passwordLower, common) {
strength.Valid = false
strength.Details = append(strength.Details,
"Password contains common words or patterns")
return strength, nil
}
}
// Check for repetitive patterns (e.g., "aaaa", "1234", "abcd")
if hasRepetitivePattern(password) {
strength.Valid = false
strength.Details = append(strength.Details,
"Password contains repetitive patterns")
return strength, nil
}
// Check for sequential patterns (e.g., "1234", "abcd", "qwerty")
if hasSequentialPattern(password) {
strength.Valid = false
strength.Details = append(strength.Details,
"Password contains sequential patterns")
return strength, nil
}
// Upper case check
if !hasUpper.MatchString(password) {
strength.Valid = false
strength.Details = append(strength.Details, "Must contain uppercase letter")
} else {
strength.Score++
}
// Lower case check
if !hasLower.MatchString(password) {
strength.Valid = false
strength.Details = append(strength.Details, "Must contain lowercase letter")
} else {
strength.Score++
}
// Number check
if !hasNumber.MatchString(password) {
strength.Valid = false
strength.Details = append(strength.Details, "Must contain number")
} else {
strength.Score++
}
// Special character check
if !hasSpecial.MatchString(password) {
strength.Valid = false
strength.Details = append(strength.Details, "Must contain special character")
} else {
strength.Score++
}
return strength, nil
}
// hasRepetitivePattern checks if password contains repetitive characters
func hasRepetitivePattern(password string) bool {
if len(password) < 4 {
return false
}
// Check for 4+ consecutive identical characters
count := 1
for i := 1; i < len(password); i++ {
if password[i] == password[i-1] {
count++
if count >= 4 {
return true
}
} else {
count = 1
}
}
return false
}
// hasSequentialPattern checks if password contains sequential characters
func hasSequentialPattern(password string) bool {
if len(password) < 4 {
return false
}
passwordLower := strings.ToLower(password)
// Check for sequential patterns (e.g., "1234", "abcd", "qwerty")
sequences := []string{
"0123456789", "9876543210",
"abcdefghijklmnopqrstuvwxyz", "zyxwvutsrqponmlkjihgfedcba",
"qwertyuiop", "poiuytrewq",
"asdfghjkl", "lkjhgfdsa",
"zxcvbnm", "mnbvcxz",
}
for _, seq := range sequences {
for i := 0; i <= len(seq)-4; i++ {
subseq := seq[i : i+4]
if strings.Contains(passwordLower, subseq) {
return true
}
}
}
return false
}
// ValidatePasswordChange validates a new password against the old password
// BE-SEC-006: Ensure new password is sufficiently different from old password
func (v *PasswordValidator) ValidatePasswordChange(newPassword, oldPassword string) (PasswordStrength, error) {
strength, err := v.Validate(newPassword)
if err != nil || !strength.Valid {
return strength, err
}
// Check if new password is too similar to old password
if oldPassword != "" {
similarity := calculateSimilarity(newPassword, oldPassword)
if similarity > 0.7 {
strength.Valid = false
strength.Details = append(strength.Details,
"New password is too similar to the old password")
return strength, nil
}
// Check if new password contains old password
if strings.Contains(strings.ToLower(newPassword), strings.ToLower(oldPassword)) {
strength.Valid = false
strength.Details = append(strength.Details,
"New password cannot contain the old password")
return strength, nil
}
}
return strength, nil
}
// calculateSimilarity calculates the similarity ratio between two strings
func calculateSimilarity(s1, s2 string) float64 {
if len(s1) == 0 && len(s2) == 0 {
return 1.0
}
if len(s1) == 0 || len(s2) == 0 {
return 0.0
}
// Simple Levenshtein-like similarity
maxLen := len(s1)
if len(s2) > maxLen {
maxLen = len(s2)
}
matches := 0
minLen := len(s1)
if len(s2) < minLen {
minLen = len(s2)
}
for i := 0; i < minLen; i++ {
if s1[i] == s2[i] {
matches++
}
}
return float64(matches) / float64(maxLen)
}