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

### 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.

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)."`
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."`
}

View File

@ -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,

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
}