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

This commit is contained in:
brent s. 2022-03-04 01:04:14 -05:00
parent 77d5271b5a
commit 1c0481824e
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
9 changed files with 215 additions and 16 deletions

View File

@ -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). 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. 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. 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. 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.

View File

@ -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)."` 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)."` 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)."` 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)."` 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)."` 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."` 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."` 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."` 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."` Count uint `short:"c" long:"count" default:"1" description:"The number of passwords to generate."`
} }

View File

@ -37,6 +37,7 @@ func main() {
ExtendedSymbols: a.ExtendSymbols, ExtendedSymbols: a.ExtendSymbols,
CountUpper: a.NumUpper, CountUpper: a.NumUpper,
CountLower: a.NumLower, CountLower: a.NumLower,
CountNumbers: a.NumNumbers,
CountSymbols: a.NumSymbols, CountSymbols: a.NumSymbols,
CountExtended: a.NumExtended, CountExtended: a.NumExtended,
DisabledChars: nil, DisabledChars: nil,

View File

@ -6,6 +6,7 @@ import (


var ( var (
ErrBadType error = errors.New("cannot typeswitch; unsupported type") ErrBadType error = errors.New("cannot typeswitch; unsupported type")
ErrTooSmall error = errors.New("password max length too short for specified required chars") 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") 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 ( import (
"crypto/rand" "crypto/rand"
"math/big" "math/big"
insecureRand "math/rand"
"sort" "sort"
"strings" "strings"
) )


// newCryptoShuffler returns a new cryptoShuffler.
func newCryptoShuffler() *cryptoShuffler {
return new(cryptoShuffler)
}

/* /*
GetCharset returns a CharSet from a set of characters. GetCharset returns a CharSet from a set of characters.


@ -86,3 +92,22 @@ func saferRandInt(max int) (randInt int, err error) {


return 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). // 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) { func (c *CharSet) Swap(i, j int) {


var iVal Char = (*c)[i] (*c)[j], (*c)[i] = (*c)[i], (*c)[j]
var jVal Char = (*c)[j]

(*c)[i] = jVal
(*c)[j] = iVal


return 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 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) { func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {


var maxMin uint var maxMin uint
@ -44,9 +44,33 @@ func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
var passLenGap uint var passLenGap uint
var passLen int var passLen int
var passAlloc []rune var passAlloc []rune
var filter *selectFilter = o.getFilter()
var isDisabled bool


// Sanity checks/error conditions. // 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 { if maxMin > o.LengthMax {
err = ErrTooSmall err = ErrTooSmall
return return
@ -71,11 +95,93 @@ func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
// And make the rune slice of that length. // And make the rune slice of that length.
passAlloc = make([]rune, passLen) passAlloc = make([]rune, passLen)


for idx, _ := range passAlloc { idx := 0
for idx < passLen {
var char Char 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 { for {
var isDisabled bool


if char, err = c.RandChar(); err != nil { if char, err = c.RandChar(); err != nil {
return return
@ -86,11 +192,13 @@ func (o *GenOpts) generatePassword(c CharSet) (password string, err error) {
} }


passAlloc[idx] = rune(char) passAlloc[idx] = rune(char)
idx++
break break
} }
} }


password = string(passAlloc) password = string(passAlloc)
passwordShuffle(&password)


return return
} }
@ -182,3 +290,17 @@ func (o *GenOpts) UnsetExplicitCharset() {


return 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 package pwgenerator


// cryptoShuffler is used to shuffle a slice in a cryptographically sane way.
type cryptoShuffler struct{}

// Char is implemented as a rune. // Char is implemented as a rune.
type Char rune type Char rune


@ -32,6 +35,8 @@ type GenOpts struct {
CountUpper uint `json:"uppers"` CountUpper uint `json:"uppers"`
// CountLower specifies how many lowercase letters (0x61 to 0x7a) should be specified at a minimum. // CountLower specifies how many lowercase letters (0x61 to 0x7a) should be specified at a minimum.
CountLower uint `json:"lowers"` 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 specifies how many symbols (0x21 to 0x7e) should be specified at a minimum.
CountSymbols uint `json:"symbols"` CountSymbols uint `json:"symbols"`
// CountExtended specifies how many extended symbols (0x80 to 0xff) should be specified at a minimum. // 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 is the collection of acceptable characters as explicitly defined by the caller, if any.
explicitCharset CharSet explicitCharset CharSet
} }

// selectFilter is used to include specified number of characters.
type selectFilter struct {
upperCounter uint
lowerCounter uint
numberCounter uint
symbolCounter uint
extendedCounter uint
}