diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 641f3a3..1d72e2b 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -10,6 +10,13 @@ import ( "github.com/wneessen/apg-go" ) +// MinimumAmountTooHigh is an error message displayed when a minimum amount of +// parameter has been set to a too high value +const MinimumAmountTooHigh = "WARNING: You have selected a minimum amount of characters that is bigger\n" + + "than 50% of the minimum password length to be generated. This can lead\n" + + "to extraordinary calculation times resulting in apg-go never finishing\n" + + "the job. Please consider lowering the value.\n\n" + func main() { c := apg.NewConfig() @@ -64,6 +71,33 @@ func main() { c.Mode = apg.ModesFromFlags(ms) } + // For the "minimum amount of" modes we need to imply at the type + // of character mode is set + if c.MinLowerCase > 0 { + if float64(c.MinLength)/2 < float64(c.MinNumeric) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeLowerCase) + } + if c.MinNumeric > 0 { + if float64(c.MinLength)/2 < float64(c.MinLowerCase) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeNumeric) + } + if c.MinSpecial > 0 { + if float64(c.MinLength)/2 < float64(c.MinSpecial) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeSpecial) + } + if c.MinUpperCase > 0 { + if float64(c.MinLength)/2 < float64(c.MinUpperCase) { + _, _ = os.Stderr.WriteString(MinimumAmountTooHigh) + } + c.Mode = apg.MaskSetMode(c.Mode, apg.ModeUpperCase) + } + // Check if algorithm is supported c.Algorithm = apg.IntToAlgo(al) if c.Algorithm == apg.AlgoUnsupported { @@ -106,10 +140,12 @@ Flags: -E CHARS List of characters to be excluded in the generated password -M [LUNSHClunshc] New style password flags - Note: new-style flags have higher priority than any of the old-style flags - -mL NUMBER Minimal amount of lower-case characters (implies -L) - -mN NUMBER Minimal amount of numeric characters (imlies -N) - -mS NUMBER Minimal amount of special characters (imlies -S) - -mU NUMBER Minimal amount of upper-case characters (imlies -U) + -mL NUMBER Minimum amount of lower-case characters (implies -L) + -mN NUMBER Minimum amount of numeric characters (imlies -N) + -mS NUMBER Minimum amount of special characters (imlies -S) + -mU NUMBER Minimum amount of upper-case characters (imlies -U) + - Note: any of the "Minimum amount of" modes may result in + extraordinarily long calculation times -C Enable complex password mode (implies -L -U -N -S and disables -H) -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) -L Toggle lower-case characters in passwords (Default: on) diff --git a/mode.go b/mode.go index 3595114..05ea088 100644 --- a/mode.go +++ b/mode.go @@ -37,8 +37,8 @@ const ( CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" // CharRangeNumeric represents all numerical characters CharRangeNumeric = "1234567890" - // CharRangeNumberHuman represents all human-readable numerical characters - CharRangeNumberHuman = "23456789" + // CharRangeNumericHuman represents all human-readable numerical characters + CharRangeNumericHuman = "23456789" // CharRangeSpecial represents all special characters CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` // CharRangeSpecialHuman represents all human-readable special characters diff --git a/random.go b/random.go index 83c995b..0ef88f2 100644 --- a/random.go +++ b/random.go @@ -27,28 +27,54 @@ var ( ErrInvalidCharRange = errors.New("provided character range is not valid or empty") ) +// CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0 +func (g *Generator) CoinFlip() int64 { + cf, _ := g.RandNum(2) + return cf +} + +// CoinFlipBool performs a simple coinflip based on the rand library and returns true or false +func (g *Generator) CoinFlipBool() bool { + return g.CoinFlip() == 1 +} + // Generate generates a password based on all the different config flags and returns // it as string type. If the generation fails, an error will be thrown func (g *Generator) Generate() (string, error) { - // Coinflip mode - if g.config.Algorithm == AlgoCoinFlip { - switch g.CoinFlipBool() { - case true: - return "Heads", nil - case false: - return "Tails", nil - } + switch g.config.Algorithm { + case AlgoCoinFlip: + return g.generateCoinFlip() + case AlgoRandom: + return g.generateRandom() + case AlgoUnsupported: + return "", fmt.Errorf("unsupported algorithm") } - - l, err := g.GetPasswordLength() - if err != nil { - return "", fmt.Errorf("failed to calculate password length: %w", err) - } - _ = l - return "", nil } +// GetPasswordLength returns the password length based on the given config +// parameters +func (g *Generator) GetPasswordLength() (int64, error) { + if g.config.FixedLength > 0 { + return g.config.FixedLength, nil + } + mil := g.config.MinLength + mal := g.config.MaxLength + if mil > mal { + mal = mil + } + diff := mal - mil + 1 + ra, err := g.RandNum(diff) + if err != nil { + return 0, err + } + l := mil + ra + if l <= 0 { + return 1, nil + } + return l, nil +} + // RandomBytes returns a byte slice of random bytes with length n that got generated by // the crypto/rand generator func (g *Generator) RandomBytes(n int64) ([]byte, error) { @@ -67,10 +93,23 @@ func (g *Generator) RandomBytes(n int64) ([]byte, error) { return b, nil } +// RandNum generates a random, non-negative number with given maximum value +func (g *Generator) RandNum(m int64) (int64, error) { + if m < 1 { + return 0, ErrInvalidLength + } + mbi := big.NewInt(m) + rn, err := rand.Int(rand.Reader, mbi) + if err != nil { + return 0, fmt.Errorf("random number generation failed: %w", err) + } + return rn.Int64(), nil +} + // RandomStringFromCharRange returns a random string of length l based of the range of characters given. // The method makes use of the crypto/random package and therfore is // cryptographically secure -func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) { +func (g *Generator) RandomStringFromCharRange(l int64, cr string) (string, error) { if l < 1 { return "", ErrInvalidLength } @@ -78,7 +117,13 @@ func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) return "", ErrInvalidCharRange } rs := strings.Builder{} - rs.Grow(l) + + // As long as the length is smaller than the max. int32 value let's grow + // the string builder to the actual size, so we need less allocations + if l <= 2147483647 { + rs.Grow(int(l)) + } + crl := len(cr) rp := make([]byte, 8) @@ -105,49 +150,146 @@ func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) return rs.String(), nil } -// RandNum generates a random, non-negative number with given maximum value -func (g *Generator) RandNum(m int64) (int64, error) { - if m < 1 { - return 0, ErrInvalidLength +// GetCharRangeFromConfig checks the Mode from the Config and returns a +// list of all possible characters that are supported by these Mode +func (g *Generator) GetCharRangeFromConfig() string { + cr := strings.Builder{} + if MaskHasMode(g.config.Mode, ModeLowerCase) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeAlphaLowerHuman) + default: + cr.WriteString(CharRangeAlphaLower) + } } - mbi := big.NewInt(m) - rn, err := rand.Int(rand.Reader, mbi) + if MaskHasMode(g.config.Mode, ModeNumeric) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeNumericHuman) + default: + cr.WriteString(CharRangeNumeric) + } + } + if MaskHasMode(g.config.Mode, ModeSpecial) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeSpecialHuman) + default: + cr.WriteString(CharRangeSpecial) + } + } + if MaskHasMode(g.config.Mode, ModeUpperCase) { + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr.WriteString(CharRangeAlphaUpperHuman) + default: + cr.WriteString(CharRangeAlphaUpper) + } + } + return cr.String() +} + +func (g *Generator) checkMinimumRequirements(pw string) bool { + ok := true + if g.config.MinLowerCase > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeAlphaLowerHuman + default: + cr = CharRangeAlphaLower + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinLowerCase { + ok = false + } + } + if g.config.MinNumeric > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeNumericHuman + default: + cr = CharRangeNumeric + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinNumeric { + ok = false + } + } + if g.config.MinSpecial > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeSpecialHuman + default: + cr = CharRangeSpecial + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinSpecial { + ok = false + } + } + if g.config.MinUpperCase > 0 { + var cr string + switch MaskHasMode(g.config.Mode, ModeHumanReadable) { + case true: + cr = CharRangeAlphaUpperHuman + default: + cr = CharRangeAlphaUpper + } + + m := 0 + for _, c := range cr { + m += strings.Count(pw, string(c)) + } + if int64(m) < g.config.MinUpperCase { + ok = false + } + } + return ok +} + +// generateCoinFlip is executed when Generate() is called with Algorithm set +// to AlgoCoinFlip +func (g *Generator) generateCoinFlip() (string, error) { + switch g.CoinFlipBool() { + case true: + return "Heads", nil + default: + return "Tails", nil + } +} + +// generateRandom is executed when Generate() is called with Algorithm set +// to AlgoRandmom +func (g *Generator) generateRandom() (string, error) { + l, err := g.GetPasswordLength() if err != nil { - return 0, fmt.Errorf("random number generation failed: %w", err) + return "", fmt.Errorf("failed to calculate password length: %w", err) + } + cr := g.GetCharRangeFromConfig() + var pw string + var ok bool + for !ok { + pw, err = g.RandomStringFromCharRange(l, cr) + if err != nil { + return "", err + } + ok = g.checkMinimumRequirements(pw) } - return rn.Int64(), nil -} -// CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0 -func (g *Generator) CoinFlip() int64 { - cf, _ := g.RandNum(2) - return cf -} - -// CoinFlipBool performs a simple coinflip based on the rand library and returns true or false -func (g *Generator) CoinFlipBool() bool { - return g.CoinFlip() == 1 -} - -// GetPasswordLength returns the password length based on the given config -// parameters -func (g *Generator) GetPasswordLength() (int64, error) { - if g.config.FixedLength > 0 { - return g.config.FixedLength, nil - } - mil := g.config.MinLength - mal := g.config.MaxLength - if mil > mal { - mal = mil - } - diff := mal - mil + 1 - ra, err := g.RandNum(diff) - if err != nil { - return 0, err - } - l := mil + ra - if l <= 0 { - return 1, nil - } - return l, nil + return pw, nil } diff --git a/random_test.go b/random_test.go index e352319..a7fc479 100644 --- a/random_test.go +++ b/random_test.go @@ -103,7 +103,7 @@ func TestGenerator_RandomBytes(t *testing.T) { func TestGenerator_RandomString(t *testing.T) { g := New(NewConfig()) - l := 32 + var l int64 = 32 * 1024 tt := []struct { name string cr string @@ -137,7 +137,7 @@ func TestGenerator_RandomString(t *testing.T) { if err != nil && !tc.sf { t.Errorf("RandomStringFromCharRange failed: %s", err) } - if len(rs) != l && !tc.sf { + if int64(len(rs)) != l && !tc.sf { t.Errorf("RandomStringFromCharRange failed. Expected length: %d, got: %d", l, len(rs)) } if strings.ContainsAny(rs, tc.nr) {