package auth import ( "errors" "unicode" "golang.org/x/crypto/bcrypt" ) const ( // BcryptCost is the bcrypt hashing cost. 12 balances security and performance. BcryptCost = 12 // MinPasswordLength is the minimum allowed password length. MinPasswordLength = 8 // MaxPasswordLength is the maximum allowed password length (bcrypt limit). MaxPasswordLength = 72 ) var ( // ErrPasswordTooShort is returned when a password is below the minimum length. ErrPasswordTooShort = errors.New("password must be at least 8 characters") // ErrPasswordTooLong is returned when a password exceeds the bcrypt limit. ErrPasswordTooLong = errors.New("password must be at most 72 characters") // ErrPasswordWeak is returned when a password lacks required character types. ErrPasswordWeak = errors.New("password must contain at least one uppercase letter, one lowercase letter, and one number") ) // HashPassword hashes a plaintext password using bcrypt. func HashPassword(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost) if err != nil { return "", err } return string(hash), nil } // CheckPassword compares a plaintext password against a bcrypt hash. // Returns true if the password matches. func CheckPassword(password, hash string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil } // ValidatePasswordStrength checks that a password meets minimum complexity requirements. // Returns nil if the password is acceptable. func ValidatePasswordStrength(password string) error { if len(password) < MinPasswordLength { return ErrPasswordTooShort } if len(password) > MaxPasswordLength { return ErrPasswordTooLong } var hasUpper, hasLower, hasDigit bool for _, r := range password { switch { case unicode.IsUpper(r): hasUpper = true case unicode.IsLower(r): hasLower = true case unicode.IsDigit(r): hasDigit = true } } if !hasUpper || !hasLower || !hasDigit { return ErrPasswordWeak } return nil }