mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-03 16:02:02 +00:00
94 lines
2.6 KiB
Go
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
|
|
}
|