From 480dcd7e242ad1f6510d1ff751fc67836afa08dd Mon Sep 17 00:00:00 2001 From: brent s Date: Thu, 3 Mar 2022 04:26:44 -0500 Subject: [PATCH] args... still needs charset minimums (how?) --- .gitignore | 42 +++++++++++++++++++ TODO | 2 + cmd/pwgen/args.go | 17 ++++++++ cmd/pwgen/main.go | 49 ++++++++++++++++++++++ go.mod | 7 ++++ go.sum | 10 +++++ pwgenerator/consts.go | 7 ++++ pwgenerator/errs.go | 4 +- pwgenerator/funcs.go | 26 ++++++++++++ pwgenerator/funcs_charset.go | 14 +++++++ pwgenerator/funcs_genopts.go | 78 +++++++++++++++++++++++++++++++++++- 11 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 cmd/pwgen/args.go create mode 100644 cmd/pwgen/main.go create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eafa7db --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +*.7z +*.bak +*.deb +*.jar +*.rar +*.run +*.sig +*.tar +*.tar.bz2 +*.tar.gz +*.tar.xz +*.tbz +*.tbz2 +*.tgz +*.txz +*.zip +.*.swp +.editix +.idea/ + +# https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test +!*test.go + +# Built binaries +bin/* +/pwgen +/cmd/pwgen/pwgen + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/TODO b/TODO index d0a15ff..d083b34 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,5 @@ +- whitespace (specifically, ' ') + - "Human Readability"? - Hash/Salted hash generator diff --git a/cmd/pwgen/args.go b/cmd/pwgen/args.go new file mode 100644 index 0000000..3899723 --- /dev/null +++ b/cmd/pwgen/args.go @@ -0,0 +1,17 @@ +package main + +// Arguments contains the invocation arguments. +type Arguments struct { + NoAlpha bool `short:"a" long:"disable-alpha" description:"If specified, do NOT include the Alphabetical (letter) charset."` + NoNum bool `short:"n" long:"disable-num" description:"If specified, do NOT include the Numerical (number) charset."` + NoSymbols bool `short:"s" long:"disable-symbols" description:"If specified, do NOT include the Simple Symbols charset."` + 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)."` + 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 uint `short:"d" long:"disable-chars" description:"If specified, this string of chars should be explicitly excluded from the charset(s)."` + 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."` + 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 new file mode 100644 index 0000000..22041b5 --- /dev/null +++ b/cmd/pwgen/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/jessevdk/go-flags" + "r00t2.io/pwgen/pwgenerator" +) + +var a Arguments + +func main() { + + var err error + var genOpts *pwgenerator.GenOpts + + if _, err = flags.Parse(&a); err != nil { + switch flagsErr := err.(type) { + case *flags.Error: + if flagsErr.Type == flags.ErrHelp { + os.Exit(0) + } + log.Panicln(err) + default: + log.Panicln(err) + } + } + + genOpts = &pwgenerator.GenOpts{ + Alpha: !a.NoAlpha, + Numeric: !a.NoNum, + Symbols: !a.NoSymbols, + ExtendedSymbols: a.ExtendSymbols, + CountUpper: a.NumUpper, + CountLower: a.NumLower, + CountSymbols: a.NumSymbols, + CountExtended: a.NumExtended, + DisabledChars: nil, + LengthMin: a.MinLen, + LengthMax: a.MaxLen, + Count: a.Count, + } + + fmt.Printf("%#v\n", a) + fmt.Printf("%#v\n", genOpts) + +} diff --git a/go.mod b/go.mod index 04978f0..5c44a08 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module r00t2.io/pwgen go 1.17 + +require ( + github.com/jessevdk/go-flags v1.5.0 + r00t2.io/goutils v1.3.1 +) + +require golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e77fc37 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +r00t2.io/goutils v1.3.1 h1:mk8B2v7fM3gYoX64CDV206+/j8Z5iSJcszcPFjuvf3o= +r00t2.io/goutils v1.3.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= +r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= diff --git a/pwgenerator/consts.go b/pwgenerator/consts.go index 5989665..821c50e 100644 --- a/pwgenerator/consts.go +++ b/pwgenerator/consts.go @@ -1,5 +1,12 @@ package pwgenerator +// Defaults. +const ( + DefCount uint = 1 + DefMaxLen uint = 256 + DefMinLin uint = 1 +) + // Pre-defined charsets. var ( // upper contains the characters from 0x41 to 0x5a ([A-Z]). diff --git a/pwgenerator/errs.go b/pwgenerator/errs.go index 429ba16..3e1738e 100644 --- a/pwgenerator/errs.go +++ b/pwgenerator/errs.go @@ -5,5 +5,7 @@ import ( ) 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") + ErrSwitchedLenLimits error = errors.New("the max password length is shorter than the minimum password length") ) diff --git a/pwgenerator/funcs.go b/pwgenerator/funcs.go index de6456e..cd73d0a 100644 --- a/pwgenerator/funcs.go +++ b/pwgenerator/funcs.go @@ -1,6 +1,9 @@ package pwgenerator import ( + "bytes" + "crypto/rand" + "math/big" "sort" ) @@ -31,3 +34,26 @@ func sortDedupe(charset *CharSet) { return } + +/* + saferRandInt uses crypto/rand instead of math/rand to get a random number. + + While this is cryptographically safer, I guarantee it's a much bigger pain to do. +*/ +func saferRandInt(max int) (randInt int, err error) { + + var max64 int64 = int64(max) + var cryptoInt *big.Int = big.NewInt(max64) + var cryptoReader *bytes.Buffer = new(bytes.Buffer) + var randInt64 int64 + + if cryptoInt, err = rand.Int(cryptoReader, cryptoInt); err != nil { + return + } + + randInt64 = cryptoInt.Int64() + + randInt = int(randInt64) + + return +} diff --git a/pwgenerator/funcs_charset.go b/pwgenerator/funcs_charset.go index 27a5695..b7d15dd 100644 --- a/pwgenerator/funcs_charset.go +++ b/pwgenerator/funcs_charset.go @@ -18,6 +18,20 @@ func (c *CharSet) Less(i, j int) (isBefore bool) { return } +// RandChar returns a random character from a CharSet. +func (c *CharSet) RandChar() (char Char, err error) { + + var selectIdx int + + if selectIdx, err = saferRandInt(len(*c) - 1); err != nil { + return + } + + char = (*c)[selectIdx] + + return +} + // String returns a string from a CharSet. func (c *CharSet) String() (s string) { diff --git a/pwgenerator/funcs_genopts.go b/pwgenerator/funcs_genopts.go index 0f46bf0..636b388 100644 --- a/pwgenerator/funcs_genopts.go +++ b/pwgenerator/funcs_genopts.go @@ -2,12 +2,86 @@ package pwgenerator import ( "strings" + + "r00t2.io/goutils/multierr" ) // Generate generates a list of passwords for a GenOpts. func (o *GenOpts) Generate() (passwords []string, err error) { - // TODO + var passwds []string = make([]string, o.Count) + var charset CharSet = o.Chars() + var errs *multierr.MultiError = multierr.NewMultiError(nil) + + if o.Count == 0 { + o.Count = DefCount + } + if o.LengthMax == 0 { + o.LengthMax = DefMaxLen + } + + for idx, _ := range passwds { + if passwds[idx], err = o.generatePassword(charset); err != nil { + errs.AddError(err) + err = nil + } + } + + passwords = passwds + + if !errs.IsEmpty() { + err = errs + } + + return +} + +// generatePassword generates a single password from CharSet c. +func (o *GenOpts) generatePassword(c CharSet) (password string, err error) { + + var maxMin uint + var trueMinLen uint + var passLenGap uint + var passLen int + var passAlloc []rune + + // Sanity checks/error conditions. + maxMin = o.CountUpper + o.CountLower + o.CountSymbols + o.CountExtended + if maxMin > o.LengthMax { + err = ErrTooSmall + return + } + if o.LengthMin > o.LengthMax { + err = ErrSwitchedLenLimits + return + } + + // Defaults. + trueMinLen = o.LengthMin + if trueMinLen == 0 { + trueMinLen = DefMinLin + } + + // Get a fixed password length... + passLenGap = o.LengthMax - trueMinLen + if passLen, err = saferRandInt(int(passLenGap)); err != nil { + return + } + passLen = passLen + int(trueMinLen) // (We need to re-add; it was subtracted above to get a zero-shifted number for start in saferRandInt.) + // And make the rune slice of that length. + passAlloc = make([]rune, passLen) + + for _, idx := range passAlloc { + var char Char + + if char, err = c.RandChar(); err != nil { + return + } + + passAlloc[idx] = rune(char) + } + + password = string(passAlloc) return } @@ -35,6 +109,8 @@ func (o *GenOpts) Chars() (chars CharSet) { chars = append(chars, extendedSymbols...) } + // TODO: Count* fields + return }