diff --git a/validate.go b/validate.go index abb28eb..697ca47 100644 --- a/validate.go +++ b/validate.go @@ -16,29 +16,7 @@ func Validate(password string, minEntropy float64) error { return nil } - hasReplace := false - hasSep := false - hasOtherSpecial := false - hasLower := false - hasUpper := false - hasDigits := false - - for _, c := range password { - switch { - case strings.ContainsRune(replaceChars, c): - hasReplace = true - case strings.ContainsRune(sepChars, c): - hasSep = true - case strings.ContainsRune(otherSpecialChars, c): - hasOtherSpecial = true - case strings.ContainsRune(lowerChars, c): - hasLower = true - case strings.ContainsRune(upperChars, c): - hasUpper = true - case strings.ContainsRune(digitsChars, c): - hasDigits = true - } - } + hasReplace, hasSep, hasOtherSpecial, hasLower, hasUpper, hasDigits := getCharacterContainment(password) allMessages := []string{} @@ -64,3 +42,72 @@ func Validate(password string, minEntropy float64) error { return errors.New("insecure password, try using a longer password") } + +var ( + // ErrInsufficientSpecialCharacters is returned when the password does not contain enough variety of special characters. + ErrInsufficientSpecialCharacters = errors.New("special characters are not used enough") + // ErrNoLowercaseLetters is returned when the password does not contain any lowercase letters. + ErrNoLowercaseLetters = errors.New("no lowercase letters are used") + // ErrNoUppercaseLetters is returned when the password does not contain any uppercase letters. + ErrNoUppercaseLetters = errors.New("no uppercase letters are used") + // ErrNoDigits is returned when the password does not contain any digits. + ErrNoDigits = errors.New("no digits are used") + // ErrShortPassword is returned when the password is too short. + ErrShortPassword = errors.New("password is too short") +) + +// ValidateWithErrorSlice is similar to Validate but returns +// a slice of errors that explain the issues with the password. +// When the password is strong enough, it returns nil. +// This function is useful for returning multiple errors separately. +func ValidateWithErrorSlice(password string, minEntropy float64) []error { + entropy := getEntropy(password) + if entropy >= minEntropy { + return nil + } + + hasReplace, hasSep, hasOtherSpecial, hasLower, hasUpper, hasDigits := getCharacterContainment(password) + + errs := []error{} + + if !hasOtherSpecial || !hasSep || !hasReplace { + errs = append(errs, ErrInsufficientSpecialCharacters) + } + if !hasLower { + errs = append(errs, ErrNoLowercaseLetters) + } + if !hasUpper { + errs = append(errs, ErrNoUppercaseLetters) + } + if !hasDigits { + errs = append(errs, ErrNoDigits) + } + + if len(errs) == 0 { + return []error{ErrShortPassword} + } + + errs = append(errs, ErrShortPassword) + return errs +} + +func getCharacterContainment(password string) (hasReplace, hasSep, hasOtherSpecial, hasLower, hasUpper, hasDigits bool) { + for _, c := range password { + switch { + case strings.ContainsRune(replaceChars, c): + hasReplace = true + case strings.ContainsRune(sepChars, c): + hasSep = true + case strings.ContainsRune(otherSpecialChars, c): + hasOtherSpecial = true + case strings.ContainsRune(lowerChars, c): + hasLower = true + case strings.ContainsRune(upperChars, c): + hasUpper = true + case strings.ContainsRune(digitsChars, c): + hasDigits = true + } + } + + return +} diff --git a/validate_test.go b/validate_test.go index 8902e0f..fede4d0 100644 --- a/validate_test.go +++ b/validate_test.go @@ -33,3 +33,43 @@ func TestValidate(t *testing.T) { t.Errorf("Wanted %v, got %v", expectedError, err) } } + +func TestValidateWithErrorSlice(t *testing.T) { + errs := ValidateWithErrorSlice("mypass", 50) + expectedErrors := []error{ErrInsufficientSpecialCharacters, ErrNoUppercaseLetters, ErrNoDigits, ErrShortPassword} + testErrorSlice(t, errs, expectedErrors) + + errs = ValidateWithErrorSlice("MYPASS", 50) + expectedErrors = []error{ErrInsufficientSpecialCharacters, ErrNoLowercaseLetters, ErrNoDigits, ErrShortPassword} + testErrorSlice(t, errs, expectedErrors) + + errs = ValidateWithErrorSlice("mypassword", 4) + if errs != nil { + t.Errorf("Errs should be nil") + } + + errs = ValidateWithErrorSlice("aGoo0dMi#oFChaR2", 80) + if errs != nil { + t.Errorf("Errs should be nil") + } + + expectedErrors = []error{ErrInsufficientSpecialCharacters, ErrNoLowercaseLetters, ErrNoUppercaseLetters, ErrShortPassword} + errs = ValidateWithErrorSlice("123", 60) + testErrorSlice(t, errs, expectedErrors) +} + +func testErrorSlice(t *testing.T, errs []error, expectedErrors []error) { + t.Helper() + + if len(errs) != len(expectedErrors) { + t.Errorf("Wanted %v, got %v", expectedErrors, errs) + return + } + + for i, err := range errs { + expectedError := expectedErrors[i] + if err.Error() != expectedError.Error() { + t.Errorf("Errs[%d]: Wanted %v, got %v", i, expectedError, err) + } + } +}