Files
warmbly/internal/pkg/crypt/validation.go
T

94 lines
2.6 KiB
Go

package crypt
import (
"bufio"
"bytes"
_ "embed"
"regexp"
"strings"
"sync"
)
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
func IsValidUUID(u string) bool {
return uuidRegex.MatchString(u)
}
func IsValidHexColor(s string) bool {
return regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`).MatchString(s)
}
// PasswordRejection says why a password was refused, so the caller can tell the
// person which rule they hit rather than restating the length rule at someone
// whose password is long but breached.
type PasswordRejection int
const (
PasswordOK PasswordRejection = iota
PasswordTooShort
PasswordTooLong
PasswordBreached
)
const (
passwordMinLength = 8
passwordMaxLength = 128
)
// breachedList is the NCSC top-100k breached passwords, reduced to the entries
// long enough to pass the length rule. See the README next to it.
//
//go:embed passwords/breached.txt
var breachedList []byte
var (
breachedOnce sync.Once
breachedSet map[string]struct{}
)
// loadBreached builds the lookup set on first use. Parsing ~46k lines costs a
// few milliseconds and only happens on the first password anyone sets.
func loadBreached() {
breachedSet = make(map[string]struct{}, 48000)
sc := bufio.NewScanner(bytes.NewReader(breachedList))
sc.Buffer(make([]byte, 0, 1024), 1024)
for sc.Scan() {
if line := strings.TrimSpace(sc.Text()); line != "" {
breachedSet[line] = struct{}{}
}
}
}
// IsBreachedPassword reports whether the password appears in the embedded list
// of commonly breached passwords. The comparison is case-insensitive, because
// capitalising the first letter of a breached password does not make it a
// different password to anyone running a cracking list.
func IsBreachedPassword(password string) bool {
breachedOnce.Do(loadBreached)
_, found := breachedSet[strings.ToLower(password)]
return found
}
// CheckPassword applies the password rules: a length band, and a prohibition on
// passwords known to have been breached (NIST SP 800-63B 5.1.1.2, which asks
// for exactly this rather than composition rules).
func CheckPassword(password string) PasswordRejection {
switch n := len([]rune(password)); {
case n < passwordMinLength:
return PasswordTooShort
case n > passwordMaxLength:
return PasswordTooLong
}
if IsBreachedPassword(password) {
return PasswordBreached
}
return PasswordOK
}
// ValidatePassword is the boolean form of CheckPassword, kept for callers that
// only need to know whether the password is acceptable.
func ValidatePassword(password string) bool {
return CheckPassword(password) == PasswordOK
}