Skip to content

Commit

Permalink
init password Generator/Sequencer with rules
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t committed Apr 9, 2024
1 parent ce936ea commit 4db662a
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 72 deletions.
10 changes: 8 additions & 2 deletions cmd/password-generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ func main() {
}

func generateRandomPasswords(charset password.Charset, numChars int, count *big.Int, printIndex bool, seed int64) {
generator, err := password.NewGenerator(charset, numChars)
generator, err := password.NewGenerator(
password.WithCharset(charset),
password.WithLength(numChars),
)
if err != nil {
fmt.Printf("ERROR: failed to instantiate generator: %v\n", err)
os.Exit(1)
Expand All @@ -93,7 +96,10 @@ func generateRandomPasswords(charset password.Charset, numChars int, count *big.
}

func generateSequencedPasswords(charset password.Charset, numChars int, count *big.Int, startIdx *big.Int, printIndex bool) {
sequencer, err := password.NewSequencer(charset, numChars)
sequencer, err := password.NewSequencer(
password.WithCharset(charset),
password.WithLength(numChars),
)
if err != nil {
fmt.Printf("ERROR: failed to instantiate generator: %v\n", err)
os.Exit(1)
Expand Down
14 changes: 13 additions & 1 deletion password/charset.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const (
AlphabetsLower Charset = "abcdefghijklmnopqrstuvwxyz"
AlphabetsUpper Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Numbers Charset = "0123456789"
Symbols Charset = "!@#$%^&*()-_=+"
Symbols Charset = "~!@#$%^&*()-_=+[{]}\\|;:,<.>/?"

AllChars = AlphaNumeric + Symbols
AlphaNumeric = Alphabets + Numbers
Expand All @@ -33,6 +33,18 @@ 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)
Expand Down
8 changes: 5 additions & 3 deletions password/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package password
import "errors"

var (
ErrEmptyCharset = errors.New("cannot generate passwords with empty charset")
ErrZeroLenPassword = errors.New("cannot generate passwords with 0 length")
ErrInvalidN = errors.New("value of N exceeds valid range")
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")
)
81 changes: 59 additions & 22 deletions password/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,55 @@ type Generator interface {
}

type generator struct {
charset []rune
charsetLen int
numChars int
pool *sync.Pool
rng *rand.Rand
charset []rune
charsetLen int
charsetSymbols []rune
charsetSymbolsLen int
minSymbols int
maxSymbols int
numChars int
pool *sync.Pool
rng *rand.Rand
}

// NewGenerator returns a password generator that implements the Generator
// interface.
func NewGenerator(charset Charset, numChars int) (Generator, error) {
if len(charset) == 0 {
return nil, ErrEmptyCharset
func NewGenerator(rules ...Rule) (Generator, error) {
g := &generator{
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
}
if numChars <= 0 {
return nil, ErrZeroLenPassword
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)
// create a storage pool with enough objects to support enough parallelism
pool := &sync.Pool{
g.pool = &sync.Pool{
New: func() any {
return make([]rune, numChars)
return make([]rune, g.numChars)
},
}
for idx := 0; idx < 25; idx++ {
pool.Put(make([]rune, numChars))
g.pool.Put(make([]rune, g.numChars))
}

return &generator{
charset: []rune(charset),
charsetLen: len(charset),
numChars: numChars,
pool: pool,
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
}, nil
// 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
}

// Generate returns a randomly generated password.
Expand All @@ -64,13 +79,35 @@ func (g *generator) Generate() string {
for idx := range password {
// generate a random new character
char := g.charset[g.rng.Intn(g.charsetLen)]
// avoid repetition of previous character
for idx > 0 && char == password[idx-1] {

// 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)]
}

// set
password[idx] = char
}

// guarantee a minimum and maximum number of symbols
if g.minSymbols > 0 || g.maxSymbols > 0 {
numSymbolsToGenerate := g.minSymbols + g.rng.Intn(g.maxSymbols-g.minSymbols)
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--
}
}

return string(password)
}

Expand Down
91 changes: 84 additions & 7 deletions password/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import (
"github.com/stretchr/testify/assert"
)

var (
testGenCharset = AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()
testGenNumChars = 12
)

func BenchmarkGenerator_Generate(b *testing.B) {
g, err := NewGenerator(testGenCharset, testGenNumChars)
g, err := NewGenerator(
WithCharset(AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()),
WithLength(12),
)
assert.Nil(b, err)
assert.NotEmpty(b, g.Generate())

Expand All @@ -24,7 +22,10 @@ func BenchmarkGenerator_Generate(b *testing.B) {
}

func TestGenerator_Generate(t *testing.T) {
g, err := NewGenerator(testGenCharset, testGenNumChars)
g, err := NewGenerator(
WithCharset(AlphaNumeric.WithoutAmbiguity().WithoutDuplicates()),
WithLength(12),
)
assert.Nil(t, err)
g.SetSeed(1)

Expand Down Expand Up @@ -55,3 +56,79 @@ func TestGenerator_Generate(t *testing.T) {
fmt.Println(sb.String())
}
}

func TestGenerator_Generate_WithSymbols(t *testing.T) {
t.Run("min 0 max 3", func(t *testing.T) {
g, err := NewGenerator(
WithCharset(Charset("abcdef123456-+!@#$%").WithoutAmbiguity().WithoutDuplicates()),
WithLength(12),
WithNumSymbols(0, 3),
)
assert.Nil(t, err)
g.SetSeed(1)

expectedPasswords := []string{
"f4e23abc6ecf",
"242$e32fd3ce",
"5ebd1bcfd12b",
"213df12f4c1c",
"54efb35ecfb5",
"ed63@ad1eb1-",
"6ecfbdcfd15b",
"bfa643bece16",
"fdf3$c3f1cba",
"c345f321f56!",
}
sb := strings.Builder{}
for idx := 0; idx < 100; idx++ {
password := g.Generate()
assert.NotEmpty(t, password)
if idx < len(expectedPasswords) {
assert.Equal(t, expectedPasswords[idx], password)
if expectedPasswords[idx] != password {
sb.WriteString(fmt.Sprintf("%#v,\n", password))
}
}
}
if sb.Len() > 0 {
fmt.Println(sb.String())
}
})

t.Run("min 1 max 3", func(t *testing.T) {
g, err := NewGenerator(
WithCharset(Charset("abcdef123456-+!@#$%").WithoutAmbiguity().WithoutDuplicates()),
WithLength(12),
WithNumSymbols(1, 3),
)
assert.Nil(t, err)
g.SetSeed(1)

expectedPasswords := []string{
"f4e23a%@6ecf",
"424e-2fd#ce4",
"ebd1bcf-12bc",
"df1@f4c1c54@",
"$cfb5aed%3da",
"1a6@cfbdcfd1",
"5bfbfa64-bec",
"16efdf!6c3f1",
"ac34#f321f5!",
"@eb5d3ef5aef",
}
sb := strings.Builder{}
for idx := 0; idx < 100; idx++ {
password := g.Generate()
assert.NotEmpty(t, password)
if idx < len(expectedPasswords) {
assert.Equal(t, expectedPasswords[idx], password)
if expectedPasswords[idx] != password {
sb.WriteString(fmt.Sprintf("%#v,\n", password))
}
}
}
if sb.Len() > 0 {
fmt.Println(sb.String())
}
})
}
60 changes: 60 additions & 0 deletions password/rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package password

// Rule controls how the Generator/Sequencer generates passwords.
type Rule func(any)

var (
defaultRules = []Rule{
WithCharset(AlphaNumeric),
WithLength(8),
}
)

// WithCharset sets the Charset the Generator/Sequencer can use.
func WithCharset(c Charset) Rule {
return func(a any) {
switch v := a.(type) {
case *generator:
v.charset = []rune(c)
case *sequencer:
v.charset = []rune(c)
}
}
}

// WithLength sets the length of the generated password.
func WithLength(l int) Rule {
return func(a any) {
switch v := a.(type) {
case *generator:
v.numChars = l
case *sequencer:
v.numChars = l
}
}
}

// WithNumSymbols controls the min/max number of symbols that can appear in the
// password.
//
// Note: This works only on a Generator and is ineffective with a Sequencer.
func WithNumSymbols(min, max int) Rule {
// sanitize min and max
if min < 0 {
min = 0
}
if max < 0 {
max = 0
}
if min > max {
min = max
}

return func(a any) {
switch v := a.(type) {
case *generator:
v.minSymbols = min
v.maxSymbols = max
}
}
}
Loading

0 comments on commit 4db662a

Please sign in to comment.