- 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%)
237 lines
5.9 KiB
Go
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)
|
|
}
|