From 1c0481824e09b809b0447945e4eedfb6af135b46 Mon Sep 17 00:00:00 2001 From: brent s Date: Fri, 4 Mar 2022 01:04:14 -0500 Subject: [PATCH] char mins done; need to shuffle some error condition checks before --- README.md | 18 +++- cmd/pwgen/args.go | 3 +- cmd/pwgen/main.go | 1 + pwgenerator/errs.go | 7 +- pwgenerator/funcs.go | 25 ++++++ pwgenerator/funcs_charset.go | 6 +- pwgenerator/funcs_cryptoshuffer.go | 27 ++++++ pwgenerator/funcs_genopts.go | 130 ++++++++++++++++++++++++++++- pwgenerator/types.go | 14 ++++ 9 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 pwgenerator/funcs_cryptoshuffer.go diff --git a/README.md b/README.md index 336a344..27363a9 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,26 @@ The author is not unique in this belief, either. For example: If you decide that you still need this functionality, however, I recommend using something like [the Babble library](https://github.com/tjarratt/babble). -### Other Tips +## PWGen Tips -#### Password Hints +### Quicker Generation + +PWGen is already really fast considering all the cryptographically-sound generation it does. + +If you need to generate a very large number of passwords, however, there are some things you can do to ensure they generate more quickly: + +* Ensure that you stick to pre-defined charsets + * This means no explicit chars defined and no excluded (disabled) chars defined; the number of those chars can affect generation time +* Use a fixed length (e.g. `-l 16 -L 16`) + +## Other Tips + +### Password Hints Many services offer "password hints". These are useless at best and provide a vulnerability at worst. If you are prompted for these and they are required (as they usually are), generate and use strong unique passwords for each question and store those "answers" in your password manager as well. This slightly weakens your account's access security (as you now have 3 -- or however many hint prompts are required -- that can be guessed instead of just 1) potentially, depending on how they implement the hint system, but there is absolutely no requirement that they be real answers. Doing so would lead to a more easily socially-engineered access of your account. -#### 2FA/MFA +### 2FA/MFA If the service offers it, enable it. No arguments or excuses. It is the single most effective action you can take to protect your account's access and is well worth the slightly added complication of an additional auth method. diff --git a/cmd/pwgen/args.go b/cmd/pwgen/args.go index f111125..202a103 100644 --- a/cmd/pwgen/args.go +++ b/cmd/pwgen/args.go @@ -8,11 +8,12 @@ type Arguments struct { ExtendSymbols bool `short:"S" long:"enable-extended-symbols" description:"If specified, include the Extended Symbols charset (these characters may cause issues in some applications)."` NumUpper uint `short:"u" long:"count-upper" description:"The number of minimum uppercase characters. If not specified, this is random (if in the charset)."` NumLower uint `short:"U" long:"count-lower" description:"The number of minimum lowercase characters. If not specified, this is random (if in the charset)."` + NumNumbers uint `short:"N" long:"count-numbers" description:"The number of minimum number characters. If not specified, this is random (if in the charset)."` NumSymbols uint `short:"y" long:"count-symbols" description:"The number of minimum simple symbol characters. If not specified, this is random (if in the charset)."` NumExtended uint `short:"Y" long:"count-extended" description:"The number of minimum extended symbol characters. If not specified, this is random (if in the charset)."` DisableChars []string `short:"d" long:"disable-chars" description:"If specified, these chars should be explicitly excluded from the charset(s). Can be specified multiple times with multiple chars per switch."` ExplicitChars []string `short:"e" long:"explicit-chars" description:"If specified, ignore all charset selection and only use these characters to select from. Can be specified multiple times."` - MinLen uint `short:"l" long:"min-length" default:"16" description:"The minimum length for passwords; use 0 for no minimum limit. Set this to the same as -L/--max-length to use a fixed length."` + MinLen uint `short:"l" long:"min-length" default:"16" description:"The minimum length for passwords; use 0 for no minimum limit. Set this to the same as -L/--max-length to use a fixed length. Must be <= -L/--max-length."` MaxLen uint `short:"L" long:"max-length" default:"64" description:"The maximum length for passwords; use 0 for no maximum limit (this is hard-capped to 256 for performance reasons). Set this to the same as -l/--min-length for a fixed length. Must be >= -l/--min-length."` Count uint `short:"c" long:"count" default:"1" description:"The number of passwords to generate."` } diff --git a/cmd/pwgen/main.go b/cmd/pwgen/main.go index 86ea589..14c197c 100644 --- a/cmd/pwgen/main.go +++ b/cmd/pwgen/main.go @@ -37,6 +37,7 @@ func main() { ExtendedSymbols: a.ExtendSymbols, CountUpper: a.NumUpper, CountLower: a.NumLower, + CountNumbers: a.NumNumbers, CountSymbols: a.NumSymbols, CountExtended: a.NumExtended, DisabledChars: nil, diff --git a/pwgenerator/errs.go b/pwgenerator/errs.go index 3e1738e..59a404f 100644 --- a/pwgenerator/errs.go +++ b/pwgenerator/errs.go @@ -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") ) diff --git a/pwgenerator/funcs.go b/pwgenerator/funcs.go index 9058365..4ee9b47 100644 --- a/pwgenerator/funcs.go +++ b/pwgenerator/funcs.go @@ -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 +} diff --git a/pwgenerator/funcs_charset.go b/pwgenerator/funcs_charset.go index b088700..6e3747a 100644 --- a/pwgenerator/funcs_charset.go +++ b/pwgenerator/funcs_charset.go @@ -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 } diff --git a/pwgenerator/funcs_cryptoshuffer.go b/pwgenerator/funcs_cryptoshuffer.go new file mode 100644 index 0000000..7322c33 --- /dev/null +++ b/pwgenerator/funcs_cryptoshuffer.go @@ -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 +} diff --git a/pwgenerator/funcs_genopts.go b/pwgenerator/funcs_genopts.go index cf884f0..4503b64 100644 --- a/pwgenerator/funcs_genopts.go +++ b/pwgenerator/funcs_genopts.go @@ -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 +} diff --git a/pwgenerator/types.go b/pwgenerator/types.go index add094c..590f70e 100644 --- a/pwgenerator/types.go +++ b/pwgenerator/types.go @@ -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 +}