char mins done; need to shuffle some error condition checks before

This commit is contained in:
2022-03-04 01:04:14 -05:00
parent 77d5271b5a
commit 1c0481824e
9 changed files with 215 additions and 16 deletions

View File

@@ -5,7 +5,8 @@ import (
)
var (
ErrBadType error = errors.New("cannot typeswitch; unsupported type")
ErrTooSmall error = errors.New("password max length too short for specified required chars")
ErrSwitchedLenLimits error = errors.New("the max password length is shorter than the minimum password length")
ErrBadType error = errors.New("cannot typeswitch; unsupported type")
ErrIncompatCharsetFilter error = errors.New("the selected minimum requirements are not possible with selected/enabled charsets")
ErrSwitchedLenLimits error = errors.New("the max password length is shorter than the minimum password length")
ErrTooSmall error = errors.New("password max length too short for specified required chars")
)

View File

@@ -3,10 +3,16 @@ package pwgenerator
import (
"crypto/rand"
"math/big"
insecureRand "math/rand"
"sort"
"strings"
)
// newCryptoShuffler returns a new cryptoShuffler.
func newCryptoShuffler() *cryptoShuffler {
return new(cryptoShuffler)
}
/*
GetCharset returns a CharSet from a set of characters.
@@ -86,3 +92,22 @@ func saferRandInt(max int) (randInt int, err error) {
return
}
// passwordShuffle shuffles a password's characters ordering so we don't have the condition satisfiers in the beginning.
func passwordShuffle(passwd *string) {
var r *insecureRand.Rand = insecureRand.New(newCryptoShuffler())
r.Shuffle(
len(*passwd),
func(i, j int) {
var chars []rune = []rune(*passwd)
chars[i], chars[j] = chars[j], chars[i]
*passwd = string(chars)
},
)
return
}

View File

@@ -82,11 +82,7 @@ func (c *CharSet) String() (s string) {
// Swap will swap the position of the item at index i and the item at index j in a CharSet (needed for sort.Interface).
func (c *CharSet) Swap(i, j int) {
var iVal Char = (*c)[i]
var jVal Char = (*c)[j]
(*c)[i] = jVal
(*c)[j] = iVal
(*c)[j], (*c)[i] = (*c)[i], (*c)[j]
return
}

View File

@@ -0,0 +1,27 @@
package pwgenerator
import (
"crypto/rand"
"encoding/binary"
)
// Int63 is used to interface as a *(math/rand).Rand but backed with a cryptographically sound generation.
func (c *cryptoShuffler) Int63() (i int64) {
var b [8]byte
_, _ = rand.Read(b[:])
// Mask is used to ensure a positive number.
i = int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1))
return
}
// Seed is used to interface as a *(math/rand).Rand.
func (c *cryptoShuffler) Seed(_ int64) {
// It's 100% OK that this is a no-op because we seed from the crypto methodology itself within cryptoShuffler.Int63 on each call.
return
}

View File

@@ -36,7 +36,7 @@ func (o *GenOpts) Generate() (passwords []string, err error) {
return
}
// generatePassword generates a single password from CharSet c.
// generatePassword generates a single password from CharSet c (plus any minimum requirements).
func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
var maxMin uint
@@ -44,9 +44,33 @@ func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
var passLenGap uint
var passLen int
var passAlloc []rune
var filter *selectFilter = o.getFilter()
var isDisabled bool
// Sanity checks/error conditions.
maxMin = o.CountUpper + o.CountLower + o.CountSymbols + o.CountExtended
if o.explicitCharset != nil && len(o.explicitCharset) != 0 {
if o.CountUpper > 0 && !o.Alpha {
err = ErrIncompatCharsetFilter
return
}
if o.CountLower > 0 && !o.Alpha {
err = ErrIncompatCharsetFilter
return
}
if o.CountNumbers > 0 && !o.Numeric {
err = ErrIncompatCharsetFilter
return
}
if o.CountSymbols > 0 && !o.Symbols {
err = ErrIncompatCharsetFilter
return
}
if o.CountExtended > 0 && !o.ExtendedSymbols {
err = ErrIncompatCharsetFilter
return
}
}
maxMin = o.CountUpper + o.CountLower + o.CountNumbers + o.CountSymbols + o.CountExtended
if maxMin > o.LengthMax {
err = ErrTooSmall
return
@@ -71,11 +95,93 @@ func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
// And make the rune slice of that length.
passAlloc = make([]rune, passLen)
for idx, _ := range passAlloc {
idx := 0
for idx < passLen {
var char Char
if o.explicitCharset != nil && len(o.explicitCharset) != 0 {
// Add upperCounter chars (if necessary).
for filter.upperCounter > 0 {
if char, err = upper.RandChar(); err != nil {
continue
}
if isDisabled, err = o.DisabledChars.Has(char); err != nil || isDisabled {
err = nil
continue
}
passAlloc[idx] = rune(char)
filter.upperCounter--
idx++
continue
}
// Add lowerCounter chars (if necessary).
for filter.lowerCounter > 0 {
if char, err = lower.RandChar(); err != nil {
continue
}
if isDisabled, err = o.DisabledChars.Has(char); err != nil || isDisabled {
err = nil
continue
}
passAlloc[idx] = rune(char)
filter.lowerCounter--
idx++
continue
}
// Add numberCounter chars (if necessary).
for filter.numberCounter > 0 {
if char, err = numeric.RandChar(); err != nil {
continue
}
if isDisabled, err = o.DisabledChars.Has(char); err != nil || isDisabled {
err = nil
continue
}
passAlloc[idx] = rune(char)
filter.numberCounter--
idx++
continue
}
// Add symbolCounter chars (if necessary).
for filter.symbolCounter > 0 {
if char, err = symbols.RandChar(); err != nil {
continue
}
if isDisabled, err = o.DisabledChars.Has(char); err != nil || isDisabled {
err = nil
continue
}
passAlloc[idx] = rune(char)
filter.symbolCounter--
idx++
continue
}
// Add extendedCounter chars (if necessary).
for filter.extendedCounter > 0 {
if char, err = extendedSymbols.RandChar(); err != nil {
continue
}
if isDisabled, err = o.DisabledChars.Has(char); err != nil || isDisabled {
err = nil
continue
}
passAlloc[idx] = rune(char)
filter.extendedCounter--
idx++
continue
}
// Break here if we've reached the full password with just the filter.
if idx > passLen {
err = ErrTooSmall
return
} else if idx == passLen {
break
}
}
// Minimum requirements satisfied; continue with default GenOpts-specific charset.
for {
var isDisabled bool
if char, err = c.RandChar(); err != nil {
return
@@ -86,11 +192,13 @@ func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
}
passAlloc[idx] = rune(char)
idx++
break
}
}
password = string(passAlloc)
passwordShuffle(&password)
return
}
@@ -182,3 +290,17 @@ func (o *GenOpts) UnsetExplicitCharset() {
return
}
// getFilter gets a filter counter.
func (o *GenOpts) getFilter() (f *selectFilter) {
f = &selectFilter{
upperCounter: o.CountUpper,
lowerCounter: o.CountLower,
numberCounter: o.CountNumbers,
symbolCounter: o.CountSymbols,
extendedCounter: o.CountExtended,
}
return
}

View File

@@ -1,5 +1,8 @@
package pwgenerator
// cryptoShuffler is used to shuffle a slice in a cryptographically sane way.
type cryptoShuffler struct{}
// Char is implemented as a rune.
type Char rune
@@ -32,6 +35,8 @@ type GenOpts struct {
CountUpper uint `json:"uppers"`
// CountLower specifies how many lowercase letters (0x61 to 0x7a) should be specified at a minimum.
CountLower uint `json:"lowers"`
// CountNumbers specifies how many numbers (0x30 to 0x39) should be specified at a minimum.
CountNumbers uint `json:"numbers"`
// CountSymbols specifies how many symbols (0x21 to 0x7e) should be specified at a minimum.
CountSymbols uint `json:"symbols"`
// CountExtended specifies how many extended symbols (0x80 to 0xff) should be specified at a minimum.
@@ -50,3 +55,12 @@ type GenOpts struct {
// explicitCharset is the collection of acceptable characters as explicitly defined by the caller, if any.
explicitCharset CharSet
}
// selectFilter is used to include specified number of characters.
type selectFilter struct {
upperCounter uint
lowerCounter uint
numberCounter uint
symbolCounter uint
extendedCounter uint
}