diff --git a/internal/validate/formvalidation.go b/internal/validate/formvalidation.go index 976ffa560..51efa320c 100644 --- a/internal/validate/formvalidation.go +++ b/internal/validate/formvalidation.go @@ -21,7 +21,6 @@ "errors" "fmt" "net/mail" - "strings" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" @@ -32,8 +31,8 @@ ) const ( - maximumPasswordLength = 256 - minimumPasswordEntropy = 60 // dictates password strength. See https://github.com/wagslane/go-password-validator + maximumPasswordLength = 72 // 72 bytes is the maximum length afforded by bcrypt. See https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. + minimumPasswordEntropy = 60 // Heuristic for password strength. See https://github.com/wagslane/go-password-validator. minimumReasonLength = 40 maximumReasonLength = 500 maximumSiteTitleLength = 40 @@ -47,23 +46,29 @@ maximumListTitleLength = 200 ) -// NewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok. +// NewPassword returns a helpful error if the given password +// is too short, too long, or not sufficiently strong. func NewPassword(password string) error { - if password == "" { - return errors.New("no password provided") - } - - if len([]rune(password)) > maximumPasswordLength { - return fmt.Errorf("password should be no more than %d chars", maximumPasswordLength) + // Ensure length is OK first. + if pwLen := len(password); pwLen == 0 { + return errors.New("no password provided / provided password was 0 bytes") + } else if pwLen > maximumPasswordLength { + return fmt.Errorf( + "password should be no more than %d bytes, provided password was %d bytes", + maximumPasswordLength, pwLen, + ) } if err := pwv.Validate(password, minimumPasswordEntropy); err != nil { - // Modify error message to include percentage requred entropy the password has - percent := int(100 * pwv.GetEntropy(password) / minimumPasswordEntropy) - return errors.New(strings.ReplaceAll( - err.Error(), - "insecure password", - fmt.Sprintf("password is only %d%% strength", percent))) + // Calculate the percentage of our desired entropy this password fulfils. + entropyPercent := int(100 * pwv.GetEntropy(password) / minimumPasswordEntropy) + + // Replace the first 17 bytes (`insecure password`) + // of the error string with our own entropy message. + entropyMsg := fmt.Sprintf("password is only %d%% strength", entropyPercent) + errMsg := entropyMsg + err.Error()[17:] + + return errors.New(errMsg) } return nil // password OK diff --git a/internal/validate/formvalidation_test.go b/internal/validate/formvalidation_test.go index a594c3763..accd48ab6 100644 --- a/internal/validate/formvalidation_test.go +++ b/internal/validate/formvalidation_test.go @@ -45,7 +45,7 @@ func (suite *ValidationTestSuite) TestCheckPasswordStrength() { err = validate.NewPassword(empty) if suite.Error(err) { - suite.Equal(errors.New("no password provided"), err) + suite.Equal(errors.New("no password provided / provided password was 0 bytes"), err) } err = validate.NewPassword(terriblePassword) @@ -75,7 +75,7 @@ func (suite *ValidationTestSuite) TestCheckPasswordStrength() { err = validate.NewPassword(tooLong) if suite.Error(err) { - suite.Equal(errors.New("password should be no more than 256 chars"), err) + suite.Equal(errors.New("password should be no more than 72 bytes, provided password was 571 bytes"), err) } err = validate.NewPassword(strongPassword)