package auth import ( "errors" "golang.org/x/crypto/bcrypt" ) const bcryptCost = 12 var ( ErrPasswordMismatch = errors.New("password mismatch") ErrPasswordTooShort = errors.New("password must be at least 8 characters") ErrPasswordTooLong = errors.New("password must be at most 72 characters") ) type PasswordHasher struct { cost int } func NewPasswordHasher() *PasswordHasher { return &PasswordHasher{cost: bcryptCost} } func (h *PasswordHasher) Hash(raw string) (string, error) { if err := ValidatePassword(raw); err != nil { return "", err } b, err := bcrypt.GenerateFromPassword([]byte(raw), h.cost) if err != nil { return "", err } return string(b), nil } func (h *PasswordHasher) Verify(hash, raw string) error { if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(raw)); err != nil { if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { return ErrPasswordMismatch } return err } return nil } // ValidatePassword enforces a minimum length and the bcrypt-imposed maximum. // bcrypt silently truncates inputs over 72 bytes, which would let a user set // a 100-character password and successfully log in with the first 72; reject // at the boundary instead. func ValidatePassword(raw string) error { if len(raw) < 8 { return ErrPasswordTooShort } if len(raw) > 72 { return ErrPasswordTooLong } return nil }