Output...
-Password # 1: "GcXO27vy"
-Password # 2: "BK9zGIPN"
-Password # 3: "o5UCF2i2"
-Password # 4: "YuoEsl4p"
-Password # 5: "fMiLzaL7"
-Password # 6: "5VpPTeJG"
-Password # 7: "LvGoxO1O"
-Password # 8: "nWD9rSaj"
-Password # 9: "qMjwMI9n"
-Password # 10: "kIbf3Wsm"
+Password # 1: "CmHii4zek_wU"
+Password # 2: "m+GicmQEnxkz"
+Password # 3: "FruTpkprFNR="
+Password # 4: "p@xjqBH3bbfi"
+Password # 5: "D(WadeVLTfhm"
+Password # 6: "uLpSFv%pMidL"
+Password # 7: "bbBQ*gypmhTx"
+Password # 8: "abshu4}HNpwE"
+Password # 9: "UjGpDsP{4mfi"
+Password # 10: "toKue=dvUPzz"
diff --git a/go.mod b/go.mod
index 51d9b47..082cf50 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/jedib0t/go-passwords
-go 1.21
+go 1.22
require github.com/stretchr/testify v1.9.0
diff --git a/password/charset.go b/password/charset.go
index e8982e9..42f1fae 100644
--- a/password/charset.go
+++ b/password/charset.go
@@ -14,7 +14,7 @@ const (
AlphabetsLower Charset = "abcdefghijklmnopqrstuvwxyz"
AlphabetsUpper Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Numbers Charset = "0123456789"
- Symbols Charset = "~!@#$%^&*()-_=+[{]}\\|;:,<.>/?"
+ Symbols Charset = "~!@#$%^&*()-_=+[{]}|;:,<.>/?"
AllChars = AlphaNumeric + Symbols
AlphaNumeric = Alphabets + Numbers
@@ -33,18 +33,6 @@ func (c Charset) Contains(r rune) bool {
return false
}
-// ExtractSymbols extracts and returns a Charset with just the symbols from the
-// source Charset.
-func (c Charset) ExtractSymbols() Charset {
- sb := strings.Builder{}
- for _, r := range c {
- if strings.Contains(string(Symbols), string(r)) {
- sb.WriteRune(r)
- }
- }
- return Charset(sb.String())
-}
-
// Shuffle reorders the Charset using the given RNG.
func (c Charset) Shuffle(rng *rand.Rand) Charset {
cRunes := []rune(c)
diff --git a/password/errors.go b/password/errors.go
index b5afc15..5dccc70 100644
--- a/password/errors.go
+++ b/password/errors.go
@@ -3,9 +3,14 @@ package password
import "errors"
var (
- ErrEmptyCharset = errors.New("cannot generate passwords with empty charset")
- ErrInvalidN = errors.New("value of N exceeds valid range")
- ErrMinSymbolsTooLong = errors.New("minimum number of symbols requested longer than password")
- ErrNoSymbolsInCharset = errors.New("found no symbols to use in charset")
- ErrZeroLenPassword = errors.New("cannot generate passwords with 0 length")
+ ErrEmptyCharset = errors.New("cannot generate passwords with empty charset")
+ ErrInvalidN = errors.New("value of N exceeds valid range")
+ ErrMinLowerCaseTooLong = errors.New("minimum number of lower-case characters requested longer than password")
+ ErrMinUpperCaseTooLong = errors.New("minimum number of upper-case characters requested longer than password")
+ ErrMinSymbolsTooLong = errors.New("minimum number of symbols requested longer than password")
+ ErrNoLowerCaseInCharset = errors.New("found no lower-case characters to use in charset")
+ ErrNoUpperCaseInCharset = errors.New("found no upper-case characters to use in charset")
+ ErrNoSymbolsInCharset = errors.New("found no symbols to use in charset")
+ ErrRequirementsNotMet = errors.New("minimum number of lower-case+upper-case+symbols requested longer than password")
+ ErrZeroLenPassword = errors.New("cannot generate passwords with 0 length")
)
diff --git a/password/generator.go b/password/generator.go
index 6ef5b75..0171af3 100644
--- a/password/generator.go
+++ b/password/generator.go
@@ -1,9 +1,10 @@
package password
import (
- "math/rand"
+ "math/rand/v2"
"sync"
"time"
+ "unicode"
)
var (
@@ -14,14 +15,17 @@ type Generator interface {
// Generate returns a randomly generated password.
Generate() string
// SetSeed overrides the seed value for the RNG.
- SetSeed(seed int64)
+ SetSeed(seed uint64)
}
type generator struct {
charset []rune
- charsetLen int
+ charsetCaseLower []rune
+ charsetCaseUpper []rune
+ charsetNonSymbols []rune
charsetSymbols []rune
- charsetSymbolsLen int
+ minLowerCase int
+ minUpperCase int
minSymbols int
maxSymbols int
numChars int
@@ -32,17 +36,18 @@ type generator struct {
// NewGenerator returns a password generator that implements the Generator
// interface.
func NewGenerator(rules ...Rule) (Generator, error) {
- g := &generator{
- rng: rand.New(rand.NewSource(time.Now().UnixNano())),
- }
+ g := &generator{}
+ g.SetSeed(uint64(time.Now().UnixNano()))
for _, opt := range append(defaultRules, rules...) {
opt(g)
}
- // init the variables
- g.charsetLen = len(g.charset)
- g.charsetSymbols = []rune(Charset(g.charset).ExtractSymbols())
- g.charsetSymbolsLen = len(g.charsetSymbols)
+ // split the charsets
+ g.charsetCaseLower = filterRunes(g.charset, unicode.IsLower)
+ g.charsetCaseUpper = filterRunes(g.charset, unicode.IsUpper)
+ g.charsetNonSymbols = filterRunes(g.charset, func(r rune) bool { return !Symbols.Contains(r) })
+ g.charsetSymbols = filterRunes(g.charset, Symbols.Contains)
+
// create a storage pool with enough objects to support enough parallelism
g.pool = &sync.Pool{
New: func() any {
@@ -53,20 +58,7 @@ func NewGenerator(rules ...Rule) (Generator, error) {
g.pool.Put(make([]rune, g.numChars))
}
- // validate the inputs
- if g.charsetLen == 0 {
- return nil, ErrEmptyCharset
- }
- if g.numChars <= 0 {
- return nil, ErrZeroLenPassword
- }
- if g.minSymbols > g.numChars {
- return nil, ErrMinSymbolsTooLong
- }
- if g.minSymbols > 0 && g.charsetSymbolsLen == 0 {
- return nil, ErrNoSymbolsInCharset
- }
- return g, nil
+ return g.sanitize()
}
// Generate returns a randomly generated password.
@@ -75,46 +67,76 @@ func (g *generator) Generate() string {
password := g.pool.Get().([]rune)
defer g.pool.Put(password)
- // overwrite the contents of the []rune and stringify it for response
- for idx := range password {
- // generate a random new character
- char := g.charset[g.rng.Intn(g.charsetLen)]
-
- // avoid repetition of previous character and ignore symbols
- for (idx > 0 && char == password[idx-1]) || Symbols.Contains(char) {
- char = g.charset[g.rng.Intn(g.charsetLen)]
+ idx := 0
+ fillPassword := func(count int, runes []rune) {
+ for ; count > 0 && idx < len(password); count-- {
+ password[idx] = runes[g.rng.IntN(len(runes))]
+ idx++
}
-
- // set
- password[idx] = char
}
+ fillPassword(g.minLowerCase, g.charsetCaseLower)
+ fillPassword(g.minUpperCase, g.charsetCaseUpper)
+ fillPassword(g.numSymbolsToGenerate(), g.charsetSymbols)
+ fillPassword(len(password)-idx, g.charsetNonSymbols)
- // guarantee a minimum and maximum number of symbols
- if g.minSymbols > 0 || g.maxSymbols > 0 {
- numSymbolsToGenerate := g.minSymbols
- if g.maxSymbols > g.minSymbols {
- numSymbolsToGenerate += g.rng.Intn(g.maxSymbols-g.minSymbols) + 1
- }
- for numSymbolsToGenerate > 0 {
- // generate a random new symbol
- char := g.charsetSymbols[g.rng.Intn(g.charsetSymbolsLen)]
-
- // find a random non-symbol location in the password
- location := g.rng.Intn(g.numChars)
- for Symbols.Contains(password[location]) {
- location = g.rng.Intn(g.numChars)
- }
-
- // set
- password[location] = char
- numSymbolsToGenerate--
- }
- }
+ // shuffle it all
+ g.rng.Shuffle(len(password), func(i, j int) {
+ password[i], password[j] = password[j], password[i]
+ })
return string(password)
}
// SetSeed overrides the seed value for the RNG.
-func (g *generator) SetSeed(seed int64) {
- g.rng = rand.New(rand.NewSource(seed))
+func (g *generator) SetSeed(seed uint64) {
+ g.rng = rand.New(rand.NewPCG(seed, seed+100))
+}
+
+func (g *generator) numSymbolsToGenerate() int {
+ if g.minSymbols > 0 || g.maxSymbols > 0 {
+ return g.rng.IntN(g.maxSymbols-g.minSymbols+1) + g.minSymbols
+ }
+ return 0
+}
+
+func (g *generator) sanitize() (Generator, error) {
+ // validate the inputs
+ if len(g.charset) == 0 {
+ return nil, ErrEmptyCharset
+ }
+ if g.numChars <= 0 {
+ return nil, ErrZeroLenPassword
+ }
+ if g.minLowerCase > 0 && len(g.charsetCaseLower) == 0 {
+ return nil, ErrNoLowerCaseInCharset
+ }
+ if g.minLowerCase > g.numChars {
+ return nil, ErrMinLowerCaseTooLong
+ }
+ if g.minUpperCase > 0 && len(g.charsetCaseUpper) == 0 {
+ return nil, ErrNoUpperCaseInCharset
+ }
+ if g.minUpperCase > g.numChars {
+ return nil, ErrMinUpperCaseTooLong
+ }
+ if g.minSymbols > g.numChars {
+ return nil, ErrMinSymbolsTooLong
+ }
+ if g.minLowerCase+g.minUpperCase+g.minSymbols > g.numChars {
+ return nil, ErrRequirementsNotMet
+ }
+ if g.minSymbols > 0 && len(g.charsetSymbols) == 0 {
+ return nil, ErrNoSymbolsInCharset
+ }
+ return g, nil
+}
+
+func filterRunes(runes []rune, truth func(r rune) bool) []rune {
+ var rsp []rune
+ for _, r := range runes {
+ if truth(r) {
+ rsp = append(rsp, r)
+ }
+ }
+ return rsp
}
diff --git a/password/generator_test.go b/password/generator_test.go
index 508f1b9..9aea4ed 100644
--- a/password/generator_test.go
+++ b/password/generator_test.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"testing"
+ "unicode"
"github.com/stretchr/testify/assert"
)
@@ -30,16 +31,16 @@ func TestGenerator_Generate(t *testing.T) {
g.SetSeed(1)
expectedPasswords := []string{
- "7mfsqtsNrNcj",
- "LRagoTFi9iGz",
- "7RP42TsEV3se",
- "ZyL9CvRu5Ged",
- "y7y3wxVnRPMG",
- "qEJcmaT4yiL6",
- "XgVmEC15ZFH1",
- "XZzNALhgjLuV",
- "WoD3jbLU92tn",
- "XZxQd37ftWbo",
+ "KkeonkPQHv4r",
+ "sHL31fveTcKB",
+ "MHnSCTtKBds2",
+ "oEqJBeZ8Qmie",
+ "G2CGWSDAQUuz",
+ "RtwGPgyAq9tN",
+ "3kPAu4cMxN8t",
+ "FgWWYrjqnx19",
+ "uCFmDFDAoLZY",
+ "pMgNoVa9z5Vv",
}
sb := strings.Builder{}
for idx := 0; idx < 100; idx++ {
@@ -57,6 +58,52 @@ func TestGenerator_Generate(t *testing.T) {
}
}
+func TestGenerator_Generate_WithAMixOfEverything(t *testing.T) {
+ g, err := NewGenerator(
+ WithCharset(AllChars.WithoutAmbiguity().WithoutDuplicates()),
+ WithLength(12),
+ WithMinLowerCase(5),
+ WithMinUpperCase(2),
+ WithNumSymbols(1, 1),
+ )
+ assert.Nil(t, err)
+ g.SetSeed(1)
+
+ expectedPasswords := []string{
+ "r{rnUqHeg5QP",
+ "m1RNe4$eXuda",
+ "tq%wKqhhTMAK",
+ "r1PkMr@qta2t",
+ "hsPv+wzGiChh",
+ "uth