package utils import ( "baron-sso-backend/internal/domain" "crypto/rand" "fmt" "slices" "strings" ) const ( lowercaseChars = "abcdefghijklmnopqrstuvwxyz" uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" numberChars = "0123456789" symbolChars = "!@#$%^&*()-_=+[]{}<>?/,.~" ) // ValidatePasswordWithPolicy validates a password against the given policy. func ValidatePasswordWithPolicy(policy *domain.PasswordPolicy, password string) error { if policy == nil { return nil } if len(password) < policy.MinLength { return fmt.Errorf("비밀번호는 최소 %d자 이상이어야 합니다", policy.MinLength) } hasLower := false hasUpper := false hasNumber := false hasSymbol := false types := 0 for _, ch := range password { switch { case ch >= 'a' && ch <= 'z': hasLower = true case ch >= 'A' && ch <= 'Z': hasUpper = true case ch >= '0' && ch <= '9': hasNumber = true default: hasSymbol = true } } if hasLower { types++ } if hasUpper { types++ } if hasNumber { types++ } if hasSymbol { types++ } if policy.MinCharacterTypes > 0 && types < policy.MinCharacterTypes { return fmt.Errorf("비밀번호는 영문 대/소문자/숫자/특수문자 중 %d가지 이상을 포함해야 합니다", policy.MinCharacterTypes) } if policy.Lowercase && !hasLower { return fmt.Errorf("비밀번호에 소문자가 포함되어야 합니다") } if policy.Uppercase && !hasUpper { return fmt.Errorf("비밀번호에 대문자가 포함되어야 합니다") } if policy.Number && !hasNumber { return fmt.Errorf("비밀번호에 숫자가 포함되어야 합니다") } if policy.NonAlphanumeric && !hasSymbol { return fmt.Errorf("비밀번호에 특수문자가 포함되어야 합니다") } return nil } // GeneratePasswordWithPolicy creates a random password that satisfies the policy. func GeneratePasswordWithPolicy(policy *domain.PasswordPolicy) (string, error) { if policy == nil { policy = &domain.PasswordPolicy{} } categories := []struct { name string required bool chars string }{ {name: "lower", required: policy.Lowercase, chars: lowercaseChars}, {name: "upper", required: policy.Uppercase, chars: uppercaseChars}, {name: "number", required: policy.Number, chars: numberChars}, {name: "symbol", required: policy.NonAlphanumeric, chars: symbolChars}, } selected := make([]string, 0, len(categories)) required := make([]string, 0, len(categories)) for _, cat := range categories { if cat.chars == "" { continue } if cat.required { required = append(required, cat.chars) } selected = append(selected, cat.chars) } if len(selected) == 0 { selected = []string{lowercaseChars, uppercaseChars, numberChars, symbolChars} } additionalTypes := policy.MinCharacterTypes - len(required) if additionalTypes > 0 { pool := make([]string, 0, len(selected)) for _, cat := range selected { isRequired := slices.Contains(required, cat) if !isRequired { pool = append(pool, cat) } } for i := 0; i < additionalTypes && len(pool) > 0; i++ { idx, err := randomIndex(len(pool)) if err != nil { return "", err } required = append(required, pool[idx]) pool = append(pool[:idx], pool[idx+1:]...) } } minLength := policy.MinLength if minLength <= 0 { minLength = 12 } if minLength < len(required) { minLength = len(required) } passwordRunes := make([]rune, 0, minLength) for _, charset := range required { ch, err := randomChar(charset) if err != nil { return "", err } passwordRunes = append(passwordRunes, ch) } var combined strings.Builder for _, charset := range selected { combined.WriteString(charset) } for len(passwordRunes) < minLength { ch, err := randomChar(combined.String()) if err != nil { return "", err } passwordRunes = append(passwordRunes, ch) } if err := shuffleRunes(passwordRunes); err != nil { return "", err } return string(passwordRunes), nil } func randomIndex(max int) (int, error) { if max <= 0 { return 0, fmt.Errorf("invalid max") } b := make([]byte, 1) for { if _, err := rand.Read(b); err != nil { return 0, err } if int(b[0]) < max*(256/max) { return int(b[0]) % max, nil } } } func randomChar(chars string) (rune, error) { idx, err := randomIndex(len(chars)) if err != nil { return 0, err } return rune(chars[idx]), nil } func shuffleRunes(values []rune) error { for i := len(values) - 1; i > 0; i-- { j, err := randomIndex(i + 1) if err != nil { return err } values[i], values[j] = values[j], values[i] } return nil }