package security import ( "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/subtle" "errors" "unicode" ) const ( PasswordSaltBytes = 16 PasswordHashBytes = 32 PasswordIterations = 600_000 ) var ErrWeakPassword = errors.New("password must be 12-128 characters and contain letters and numbers") func ValidatePassword(password string) error { runes := []rune(password) if len(runes) < 12 || len(runes) > 128 { return ErrWeakPassword } var letter, number bool for _, value := range runes { letter = letter || unicode.IsLetter(value) number = number || unicode.IsNumber(value) } if !letter || !number { return ErrWeakPassword } return nil } func HashPassword(password string) (hash, salt []byte, iterations int, err error) { if err := ValidatePassword(password); err != nil { return nil, nil, 0, err } salt = make([]byte, PasswordSaltBytes) if _, err := rand.Read(salt); err != nil { return nil, nil, 0, err } hash, err = pbkdf2.Key(sha256.New, password, salt, PasswordIterations, PasswordHashBytes) if err != nil { return nil, nil, 0, err } return hash, salt, PasswordIterations, nil } func VerifyPassword(password string, expectedHash, salt []byte, iterations int) bool { if len(expectedHash) != PasswordHashBytes || len(salt) != PasswordSaltBytes || iterations < 100_000 || iterations > 10_000_000 || len([]rune(password)) > 128 { return false } actual, err := pbkdf2.Key(sha256.New, password, salt, iterations, len(expectedHash)) if err != nil { return false } return subtle.ConstantTimeCompare(actual, expectedHash) == 1 }