package auth import ( "errors" "gardomatic.kleiax.de/internal/platform/validate" "golang.org/x/crypto/bcrypt" ) // Password holds a bcrypt hash and, while constructing a new password, its // plaintext value for policy validation. Plaintext is never exposed. type Password struct { plaintext *string hash []byte } // NewPassword reconstructs a password value from an existing bcrypt hash. func NewPassword(hash []byte) *Password { return &Password{hash: hash} } // Set hashes plaintextPassword and replaces the stored hash. func (p *Password) Set(plaintextPassword string) error { hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12) if err != nil { return err } p.plaintext = &plaintextPassword p.hash = hash return nil } // Get returns a defensive copy of the bcrypt hash. func (p *Password) Get() []byte { return p.hash } // Matches reports whether plaintextPassword matches the stored bcrypt hash. func (p *Password) Matches(plaintextPassword string) (bool, error) { err := bcrypt.CompareHashAndPassword(p.hash, []byte(plaintextPassword)) if err != nil { switch { case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword): return false, nil default: return false, err } } return true, nil } // Validate adds password-hash validation errors to v. func (p *Password) Validate(v *validate.Validator) { if p.plaintext != nil { ValidatePasswordPlaintext(v, *p.plaintext) } if p.hash == nil { panic("missing password hash for user") } } // ValidatePasswordPlaintext applies the password policy to plaintext input. func ValidatePasswordPlaintext(v *validate.Validator, plaintext string) { v.Check(plaintext != "", "password", "must be provided") v.Check(len(plaintext) >= 8, "password", "must be at least 8 bytes long") v.Check(len(plaintext) <= 72, "password", "must not be more than 72 bytes long") }