package validators import ( "fmt" "os" "regexp" "strconv" "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", } ) // PasswordPolicyConfig holds configurable password policy settings. // F015: Configurable password policy. type PasswordPolicyConfig struct { MinLength int // Minimum password length (default 12) MaxLength int // Maximum password length (default 128) RequireUpper bool // Require uppercase letter (default true) RequireLower bool // Require lowercase letter (default true) RequireNumber bool // Require digit (default true) RequireSpecial bool // Require special character (default true) } // DefaultPasswordPolicy returns the default (strict) policy. func DefaultPasswordPolicy() PasswordPolicyConfig { return PasswordPolicyConfig{ MinLength: 12, MaxLength: 128, RequireUpper: true, RequireLower: true, RequireNumber: true, RequireSpecial: true, } } // PasswordValidator valide la force d'un mot de passe type PasswordValidator struct { MinLength int Policy PasswordPolicyConfig } // NewPasswordValidator crée une nouvelle instance de PasswordValidator func NewPasswordValidator() *PasswordValidator { p := DefaultPasswordPolicy() return &PasswordValidator{MinLength: p.MinLength, Policy: p} } // NewPasswordValidatorWithPolicy creates a validator with a custom policy. // F015: Configurable password policy. func NewPasswordValidatorWithPolicy(policy PasswordPolicyConfig) *PasswordValidator { if policy.MinLength < 8 { policy.MinLength = 8 // Absolute minimum } if policy.MaxLength <= 0 { policy.MaxLength = 128 } return &PasswordValidator{MinLength: policy.MinLength, Policy: policy} } // 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, fmt.Sprintf("Password must be at least %d characters long", v.MinLength)) return strength, nil } // Maximum length check (prevent DoS) maxLen := v.Policy.MaxLength if maxLen <= 0 { maxLen = 128 } if len(password) > maxLen { strength.Valid = false strength.Details = append(strength.Details, fmt.Sprintf("Password must be less than %d characters", maxLen)) return strength, nil } // Check for common weak passwords // Only reject if password IS exactly a common password // This is less aggressive than checking if password contains common words, // which would reject valid passwords like "VerySecurePassword123!@#" passwordLower := strings.ToLower(password) for _, common := range commonPasswords { // Exact match only if 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 (F015: configurable) if v.Policy.RequireUpper { if !hasUpper.MatchString(password) { strength.Valid = false strength.Details = append(strength.Details, "Must contain uppercase letter") } else { strength.Score++ } } // Lower case check (F015: configurable) if v.Policy.RequireLower { if !hasLower.MatchString(password) { strength.Valid = false strength.Details = append(strength.Details, "Must contain lowercase letter") } else { strength.Score++ } } // Number check (F015: configurable) if v.Policy.RequireNumber { if !hasNumber.MatchString(password) { strength.Valid = false strength.Details = append(strength.Details, "Must contain number") } else { strength.Score++ } } // Special character check (F015: configurable) if v.Policy.RequireSpecial { 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) } // PasswordPolicyFromEnv reads password policy config from environment variables. // F015: Configurable password policy. // Env vars: // - PASSWORD_MIN_LENGTH (default 12, minimum 8) // - PASSWORD_MAX_LENGTH (default 128) // - PASSWORD_REQUIRE_UPPER (default true) // - PASSWORD_REQUIRE_LOWER (default true) // - PASSWORD_REQUIRE_NUMBER (default true) // - PASSWORD_REQUIRE_SPECIAL (default true) func PasswordPolicyFromEnv() PasswordPolicyConfig { p := DefaultPasswordPolicy() if v := os.Getenv("PASSWORD_MIN_LENGTH"); v != "" { if n, err := strconv.Atoi(v); err == nil && n >= 8 { p.MinLength = n } } if v := os.Getenv("PASSWORD_MAX_LENGTH"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { p.MaxLength = n } } if v := os.Getenv("PASSWORD_REQUIRE_UPPER"); v != "" { p.RequireUpper = parseBool(v, true) } if v := os.Getenv("PASSWORD_REQUIRE_LOWER"); v != "" { p.RequireLower = parseBool(v, true) } if v := os.Getenv("PASSWORD_REQUIRE_NUMBER"); v != "" { p.RequireNumber = parseBool(v, true) } if v := os.Getenv("PASSWORD_REQUIRE_SPECIAL"); v != "" { p.RequireSpecial = parseBool(v, true) } return p } func parseBool(s string, defaultVal bool) bool { b, err := strconv.ParseBool(s) if err != nil { return defaultVal } return b }