github.com/cs3org/reva/v2@v2.27.7/pkg/password/password_policies.go (about)

     1  package password
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  	"unicode/utf8"
     9  )
    10  
    11  // https://owasp.org/www-community/password-special-characters
    12  var _defaultSpecialCharacters = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
    13  
    14  // Validator describes the interface providing a password Validate method
    15  type Validator interface {
    16  	Validate(str string) error
    17  }
    18  
    19  // Policies represents a password validation rules
    20  type Policies struct {
    21  	minCharacters           int
    22  	minLowerCaseCharacters  int
    23  	minUpperCaseCharacters  int
    24  	minDigits               int
    25  	minSpecialCharacters    int
    26  	bannedPasswordsList     map[string]struct{}
    27  	digitsRegexp            *regexp.Regexp
    28  	specialCharactersRegexp *regexp.Regexp
    29  }
    30  
    31  // NewPasswordPolicy returns a new NewPasswordPolicy instance
    32  func NewPasswordPolicy(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int, bannedPasswordsList map[string]struct{}) Validator {
    33  	p := &Policies{
    34  		minCharacters:          minCharacters,
    35  		minLowerCaseCharacters: minLowerCaseCharacters,
    36  		minUpperCaseCharacters: minUpperCaseCharacters,
    37  		minDigits:              minDigits,
    38  		minSpecialCharacters:   minSpecialCharacters,
    39  		bannedPasswordsList:    bannedPasswordsList,
    40  	}
    41  
    42  	p.digitsRegexp = regexp.MustCompile("[0-9]")
    43  	p.specialCharactersRegexp = regexp.MustCompile(specialCharactersExp(_defaultSpecialCharacters))
    44  	return p
    45  }
    46  
    47  // Validate implements a password validation regarding the policy
    48  func (s Policies) Validate(str string) error {
    49  	var allErr error
    50  	if !utf8.ValidString(str) {
    51  		return fmt.Errorf("the password contains invalid characters")
    52  	}
    53  	err := s.validateBannedList(str)
    54  	if err != nil {
    55  		return err
    56  	}
    57  	err = s.validateCharacters(str)
    58  	if err != nil {
    59  		allErr = errors.Join(allErr, err)
    60  	}
    61  	err = s.validateLowerCase(str)
    62  	if err != nil {
    63  		allErr = errors.Join(allErr, err)
    64  	}
    65  	err = s.validateUpperCase(str)
    66  	if err != nil {
    67  		allErr = errors.Join(allErr, err)
    68  	}
    69  	err = s.validateDigits(str)
    70  	if err != nil {
    71  		allErr = errors.Join(allErr, err)
    72  	}
    73  	err = s.validateSpecialCharacters(str)
    74  	if err != nil {
    75  		allErr = errors.Join(allErr, err)
    76  	}
    77  	if allErr != nil {
    78  		return allErr
    79  	}
    80  	return nil
    81  }
    82  
    83  func (s Policies) validateBannedList(str string) error {
    84  	if len(s.bannedPasswordsList) == 0 {
    85  		return nil
    86  	}
    87  	if _, ok := s.bannedPasswordsList[str]; ok {
    88  		return fmt.Errorf("unfortunately, your password is commonly used. please pick a harder-to-guess password for your safety")
    89  	}
    90  	return nil
    91  }
    92  
    93  func (s Policies) validateCharacters(str string) error {
    94  	if s.count(str) < s.minCharacters {
    95  		return fmt.Errorf("at least %d characters are required", s.minCharacters)
    96  	}
    97  	return nil
    98  }
    99  
   100  func (s Policies) validateLowerCase(str string) error {
   101  	if s.countLowerCaseCharacters(str) < s.minLowerCaseCharacters {
   102  		return fmt.Errorf("at least %d lowercase letters are required", s.minLowerCaseCharacters)
   103  	}
   104  	return nil
   105  }
   106  
   107  func (s Policies) validateUpperCase(str string) error {
   108  	if s.countUpperCaseCharacters(str) < s.minUpperCaseCharacters {
   109  		return fmt.Errorf("at least %d uppercase letters are required", s.minUpperCaseCharacters)
   110  	}
   111  	return nil
   112  }
   113  
   114  func (s Policies) validateDigits(str string) error {
   115  	if s.countDigits(str) < s.minDigits {
   116  		return fmt.Errorf("at least %d numbers are required", s.minDigits)
   117  	}
   118  	return nil
   119  }
   120  
   121  func (s Policies) validateSpecialCharacters(str string) error {
   122  	if s.countSpecialCharacters(str) < s.minSpecialCharacters {
   123  		return fmt.Errorf("at least %d special characters are required %s", s.minSpecialCharacters, _defaultSpecialCharacters)
   124  	}
   125  	return nil
   126  }
   127  
   128  func (s Policies) count(str string) int {
   129  	return utf8.RuneCount([]byte(str))
   130  }
   131  
   132  func (s Policies) countLowerCaseCharacters(str string) int {
   133  	var count int
   134  	for _, c := range str {
   135  		if strings.ToLower(string(c)) == string(c) && strings.ToUpper(string(c)) != string(c) {
   136  			count++
   137  		}
   138  	}
   139  	return count
   140  }
   141  
   142  func (s Policies) countUpperCaseCharacters(str string) int {
   143  	var count int
   144  	for _, c := range str {
   145  		if strings.ToUpper(string(c)) == string(c) && strings.ToLower(string(c)) != string(c) {
   146  			count++
   147  		}
   148  	}
   149  	return count
   150  }
   151  
   152  func (s Policies) countDigits(str string) int {
   153  	return len(s.digitsRegexp.FindAllStringIndex(str, -1))
   154  }
   155  
   156  func (s Policies) countSpecialCharacters(str string) int {
   157  	if s.specialCharactersRegexp == nil {
   158  		return 0
   159  	}
   160  	res := s.specialCharactersRegexp.FindAllStringIndex(str, -1)
   161  	return len(res)
   162  }
   163  
   164  func specialCharactersExp(str string) string {
   165  	// escape the '-' character because it is a not meta-characters but, they are special inside of []
   166  	return "[" + strings.ReplaceAll(regexp.QuoteMeta(str), "-", `\-`) + "]"
   167  }