첫 커밋: 로컬 프로젝트 업로드
This commit is contained in:
200
baron-sso/backend/internal/utils/password_policy.go
Normal file
200
baron-sso/backend/internal/utils/password_policy.go
Normal file
@@ -0,0 +1,200 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user