Skip to content

Commit

Permalink
support min lower/upper case character rules
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t committed Apr 18, 2024
1 parent a8f72a9 commit 27a99cd
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 124 deletions.
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ goos: linux
goarch: amd64
pkg: github.com/jedib0t/go-passwords/password
cpu: AMD Ryzen 9 5900X 12-Core Processor
BenchmarkGenerator_Generate-12 6537692 171.2 ns/op 40 B/op 2 allocs/op
BenchmarkSequencer_GotoN-12 4000642 290.0 ns/op 32 B/op 3 allocs/op
BenchmarkSequencer_Next-12 13113400 88.12 ns/op 16 B/op 1 allocs/op
BenchmarkSequencer_NextN-12 6000421 196.9 ns/op 32 B/op 3 allocs/op
BenchmarkSequencer_Prev-12 12717573 92.34 ns/op 16 B/op 1 allocs/op
BenchmarkSequencer_PrevN-12 3909879 302.3 ns/op 32 B/op 3 allocs/op
BenchmarkGenerator_Generate-12 6245260 188.2 ns/op 40 B/op 2 allocs/op
BenchmarkSequencer_GotoN-12 4359440 270.6 ns/op 32 B/op 3 allocs/op
BenchmarkSequencer_Next-12 13632730 83.67 ns/op 16 B/op 1 allocs/op
BenchmarkSequencer_NextN-12 6608569 181.5 ns/op 32 B/op 3 allocs/op
BenchmarkSequencer_Prev-12 13509426 87.51 ns/op 16 B/op 1 allocs/op
BenchmarkSequencer_PrevN-12 4266948 276.8 ns/op 32 B/op 3 allocs/op
PASS
ok github.com/jedib0t/go-passwords/password 8.178s
```
Expand All @@ -29,8 +29,11 @@ ok github.com/jedib0t/go-passwords/password 8.178s
### Random Passwords
```golang
generator, err := password.NewGenerator(
password.WithCharset(password.AlphaNumeric),
password.WithLength(8),
password.WithCharset(password.AllChars.WithoutAmbiguity().WithoutDuplicates()),
password.WithLength(12),
password.WithMinLowerCase(5),
password.WithMinpperCase(2),
password.WithNumSymbols(1, 1),
)
if err != nil {
panic(err.Error())
Expand All @@ -42,16 +45,16 @@ ok github.com/jedib0t/go-passwords/password 8.178s
<details>
<summary>Output...</summary>
<pre>
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"
</pre>
</details>

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/jedib0t/go-passwords

go 1.21
go 1.22

require github.com/stretchr/testify v1.9.0

Expand Down
14 changes: 1 addition & 13 deletions 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,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)
Expand Down
15 changes: 10 additions & 5 deletions password/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
140 changes: 81 additions & 59 deletions password/generator.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package password

import (
"math/rand"
"math/rand/v2"
"sync"
"time"
"unicode"
)

var (
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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.
Expand All @@ -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
}
Loading

0 comments on commit 27a99cd

Please sign in to comment.