169 lines
4.0 KiB
Go
169 lines
4.0 KiB
Go
package passwords
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
lowercaseChars = "abcdefghijkmnopqrstuvwxyz"
|
|
uppercaseChars = "ABCDEFGHJKLMNPQRSTUVWXYZ"
|
|
digitChars = "23456789"
|
|
symbolChars = "!@#$%^&*()-_=+[]{}<>?/."
|
|
)
|
|
|
|
var ErrImpossibleProfile = errors.New("impossible password profile")
|
|
|
|
type Profile struct {
|
|
Name string
|
|
Length int
|
|
Lowercase bool
|
|
Uppercase bool
|
|
Digits bool
|
|
Symbols bool
|
|
MinLowercase int
|
|
MinUppercase int
|
|
MinDigits int
|
|
MinSymbols int
|
|
ExcludeSimilar bool
|
|
}
|
|
|
|
func DefaultProfiles() map[string]Profile {
|
|
return map[string]Profile{
|
|
"strong": {
|
|
Name: "strong",
|
|
Length: 24,
|
|
Lowercase: true,
|
|
Uppercase: true,
|
|
Digits: true,
|
|
Symbols: true,
|
|
MinLowercase: 2,
|
|
MinUppercase: 2,
|
|
MinDigits: 2,
|
|
MinSymbols: 2,
|
|
ExcludeSimilar: true,
|
|
},
|
|
"memorable": {
|
|
Name: "memorable",
|
|
Length: 20,
|
|
Lowercase: true,
|
|
Uppercase: true,
|
|
Digits: true,
|
|
Symbols: false,
|
|
MinLowercase: 4,
|
|
MinUppercase: 2,
|
|
MinDigits: 2,
|
|
ExcludeSimilar: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
func Generate(profile Profile) (string, error) {
|
|
if err := validateProfile(profile); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var chars []byte
|
|
var pool strings.Builder
|
|
if profile.Lowercase {
|
|
pool.WriteString(lowercaseChars)
|
|
chars = append(chars, mustRandomChars(lowercaseChars, profile.MinLowercase)...)
|
|
}
|
|
if profile.Uppercase {
|
|
pool.WriteString(uppercaseChars)
|
|
chars = append(chars, mustRandomChars(uppercaseChars, profile.MinUppercase)...)
|
|
}
|
|
if profile.Digits {
|
|
pool.WriteString(digitChars)
|
|
chars = append(chars, mustRandomChars(digitChars, profile.MinDigits)...)
|
|
}
|
|
if profile.Symbols {
|
|
pool.WriteString(symbolChars)
|
|
chars = append(chars, mustRandomChars(symbolChars, profile.MinSymbols)...)
|
|
}
|
|
|
|
allChars := pool.String()
|
|
for len(chars) < profile.Length {
|
|
ch, err := randomChar(allChars)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
chars = append(chars, ch)
|
|
}
|
|
|
|
if err := shuffle(chars); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(chars), nil
|
|
}
|
|
|
|
func validateProfile(profile Profile) error {
|
|
if profile.Length <= 0 {
|
|
return fmt.Errorf("%w: length must be positive", ErrImpossibleProfile)
|
|
}
|
|
|
|
required := profile.MinLowercase + profile.MinUppercase + profile.MinDigits + profile.MinSymbols
|
|
if required > profile.Length {
|
|
return fmt.Errorf("%w: minimum character counts exceed length", ErrImpossibleProfile)
|
|
}
|
|
|
|
if profile.MinLowercase > 0 && !profile.Lowercase {
|
|
return fmt.Errorf("%w: lowercase disabled with lowercase minimum", ErrImpossibleProfile)
|
|
}
|
|
if profile.MinUppercase > 0 && !profile.Uppercase {
|
|
return fmt.Errorf("%w: uppercase disabled with uppercase minimum", ErrImpossibleProfile)
|
|
}
|
|
if profile.MinDigits > 0 && !profile.Digits {
|
|
return fmt.Errorf("%w: digits disabled with digit minimum", ErrImpossibleProfile)
|
|
}
|
|
if profile.MinSymbols > 0 && !profile.Symbols {
|
|
return fmt.Errorf("%w: symbols disabled with symbol minimum", ErrImpossibleProfile)
|
|
}
|
|
|
|
if !profile.Lowercase && !profile.Uppercase && !profile.Digits && !profile.Symbols {
|
|
return fmt.Errorf("%w: no character sets enabled", ErrImpossibleProfile)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func mustRandomChars(chars string, count int) []byte {
|
|
if count <= 0 {
|
|
return nil
|
|
}
|
|
|
|
out := make([]byte, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
ch, err := randomChar(chars)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
out = append(out, ch)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func randomChar(chars string) (byte, error) {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("random index: %w", err)
|
|
}
|
|
return chars[n.Int64()], nil
|
|
}
|
|
|
|
func shuffle(chars []byte) error {
|
|
for i := len(chars) - 1; i > 0; i-- {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
|
if err != nil {
|
|
return fmt.Errorf("shuffle password: %w", err)
|
|
}
|
|
j := int(n.Int64())
|
|
chars[i], chars[j] = chars[j], chars[i]
|
|
}
|
|
return nil
|
|
}
|