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 }