internal/crypto/password.go
Rama main · réplica de lectura del repositorio original.
package crypto
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/crypto/bcrypt"
)
// PasswordPolicyMinLength is the minimum length accepted for a dashboard user
// password. The policy below is mirrored in the dashboard
// (apps/dashboard/src/lib/password-policy.ts) so the UI can give per-rule
// feedback before the server is ever hit, keep both in sync.
const PasswordPolicyMinLength = 8
// PasswordPolicyDescription spells out every rule at once, for messages that
// must state the full policy up front, e.g. the seed migration's fail-fast,
// where the operator has no UI checklist to look at.
var PasswordPolicyDescription = fmt.Sprintf(
"at least %d characters, an uppercase letter, a lowercase letter, a digit and a special character",
PasswordPolicyMinLength,
)
// ValidatePasswordPolicy rejects passwords that miss any of the policy rules:
// minimum length, one uppercase letter, one lowercase letter, one digit and
// one special (non-alphanumeric) character. The error lists every failing
// rule at once so the user does not discover them one 400 at a time.
func ValidatePasswordPolicy(password string) error {
var hasUpper, hasLower, hasDigit, hasSpecial bool
for _, r := range password {
switch {
case unicode.IsUpper(r):
hasUpper = true
case unicode.IsLower(r):
hasLower = true
case unicode.IsDigit(r):
hasDigit = true
default:
hasSpecial = true
}
}
var missing []string
// Count runes, not bytes: "Ääää" is 8 bytes but only 4 characters, and the
// dashboard mirror counts characters too.
if utf8.RuneCountInString(password) < PasswordPolicyMinLength {
missing = append(missing, fmt.Sprintf("at least %d characters", PasswordPolicyMinLength))
}
if !hasUpper {
missing = append(missing, "an uppercase letter")
}
if !hasLower {
missing = append(missing, "a lowercase letter")
}
if !hasDigit {
missing = append(missing, "a digit")
}
if !hasSpecial {
missing = append(missing, "a special character")
}
if len(missing) > 0 {
return fmt.Errorf("password does not meet the policy, it needs %s", strings.Join(missing, ", "))
}
return nil
}
// HashPassword hashes a user password with bcrypt. This is deliberately not
// HashPlaintextAPIKey: API keys carry 32 bytes of entropy so a fast SHA-256 is
// fine, but user passwords are low-entropy and need a slow, salted KDF.
func HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hash), nil
}
// VerifyPassword reports whether password matches the stored bcrypt hash.
func VerifyPassword(hashedPassword string, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
}