From ac97b94ec96f13b31d111d6a98f2af954f0ed84b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 4 Aug 2023 16:02:58 +0200 Subject: [PATCH] Refactor generator and add config options Refactored the generator to include a new config option, changed function signatures to follow the new structure, and renamed the function 'RandomString' to 'RandomStringFromCharRange' for clarity. Also, added a new mode and algorithm feature to enhance password generation. Furthermore, added several tests for new features and configurations. Adapted the CLI to use the new configuration approach. This refactoring was necessary to improve the customizability and clarity of the password generation process. Fixed minor issues and added '.gitignore' for clean commits in the future. --- .gitignore | 29 +++++++++++++++++++++ algo.go | 29 +++++++++++++++++++++ apg.go | 11 ++++++-- cmd/apg/apg.go | 63 +++++++++++++++++++++++++++++++++++++++------ config.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ config_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ mode.go | 51 +++++++++++++++++++++++++++++++++++++ mode_test.go | 40 +++++++++++++++++++++++++++++ random.go | 25 +++--------------- random_test.go | 28 ++++++++++---------- 10 files changed, 366 insertions(+), 46 deletions(-) create mode 100644 .gitignore create mode 100644 algo.go create mode 100644 config.go create mode 100644 config_test.go create mode 100644 mode.go create mode 100644 mode_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f1a16a --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2022 Winni Neessen +# +# SPDX-License-Identifier: CC0-1.0 + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Local testfiles and auth data +.auth +examples/* + +# SonarQube +.scannerwork/ + +# IDEA specific ignores +.idea/ diff --git a/algo.go b/algo.go new file mode 100644 index 0000000..a1be7be --- /dev/null +++ b/algo.go @@ -0,0 +1,29 @@ +package apg + +// Algorithm is a type wrapper for an int type to represent different +// password generation algorithm +type Algorithm int + +const ( + // Pronouncable represents the algorithm for pronouncable passwords + // (koremutake syllables) + Pronouncable Algorithm = iota + // Random represents the algorithm for purely random passwords according + // to the provided password modes/flags + Random + // Unsupported represents an unsupported algorithm + Unsupported +) + +// IntToAlgo takes an int value as input and returns the corresponding +// Algorithm +func IntToAlgo(a int) Algorithm { + switch a { + case 0: + return Pronouncable + case 1: + return Random + default: + return Unsupported + } +} diff --git a/apg.go b/apg.go index 496a575..9aba57a 100644 --- a/apg.go +++ b/apg.go @@ -1,12 +1,19 @@ package apg +// VERSION represents the version string +const VERSION = "2.0.0" + // Generator is the password generator type of the APG package type Generator struct { // charRange is the range of character used for the charRange string + // config is a pointer to the apg config instance + config *Config } // New returns a new password Generator type -func New() *Generator { - return &Generator{} +func New(c *Config) *Generator { + return &Generator{ + config: c, + } } diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 12ec9d0..b8e361b 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -3,18 +3,65 @@ package main import ( - "fmt" + "flag" "os" "github.com/wneessen/apg-go" ) func main() { - g := apg.New() - rb, err := g.RandomBytes(8) - if err != nil { - fmt.Println("ERROR", err) - os.Exit(1) - } - fmt.Printf("Random: %#v\n", rb) + c := apg.NewConfig() + + // Configure and parse the CLI flags + flag.Int64Var(&c.MinLength, "m", c.MinLength, "") + flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "") + flag.Int64Var(&c.NumberPass, "n", c.NumberPass, "") + flag.Usage = usage + flag.Parse() + + /* + g := apg.New(c) + rb, err := g.RandomBytes(c.MinLength) + if err != nil { + fmt.Println("ERROR", err) + os.Exit(1) + } + fmt.Printf("Random: %#v\n", rb) + + */ +} + +// usage is used by the flag package to display the CLI usage message +func usage() { + // Usage text + const ut = `apg-go // A "Automated Password Generator"-clone +Copyleft (c) 2021-2023 Winni Neessen + +apg [-a ] [-m ] [-x ] [-L] [-U] [-N] [-S] [-H] [-C] + [-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t] + +Options: + -a ALGORITH Choose the password generation algorithm (Default: 1) + - 0: pronounceable password generation (koremutake syllables) + - 1: random password generation according to password modes/flags + -m LENGTH Minimum length of the password to be generated (Default: 12) + -x LENGTH Maximum length of the password to be generated (Default: 20) + -n NUMBER Amount of password to be generated (Default: 6) + -E CHARS List of characters to be excluded in the generated password + -M [LUNSHClunshc] New style password parameters (upper case: on, lower case: off) + -L Use lower case characters in passwords (Default: on) + -U Use upper case characters in passwords (Default: on) + -N Use numeric characters in passwords (Default: on) + -S Use special characters in passwords (Default: off) + -H Avoid ambiguous characters in passwords (i. e.: 1, l, I, O, 0) (Default: off) + -C Enable complex password mode (implies -L -U -N -S and disables -H) (Default: off) + -l Spell generated passwords in phonetic alphabet (Default: off) + -p Check the HIBP database if the generated passwords was found in a leak before (Default: off) + - Note: this feature requires internet connectivity + -h Show this help text + -v Show version string + +` + + _, _ = os.Stderr.WriteString(ut) } diff --git a/config.go b/config.go new file mode 100644 index 0000000..286dde6 --- /dev/null +++ b/config.go @@ -0,0 +1,67 @@ +package apg + +// List of default values for Config instances +const ( + // DefaultMinLength reflects the default minimum length of a generated password + DefaultMinLength int64 = 12 + // DefaultMaxLength reflects the default maximum length of a generated password + DefaultMaxLength int64 = 20 + // DefaultNumberPass reflects the default amount of passwords returned by the generator + DefaultNumberPass int64 = 6 +) + +// Config represents the apg.Generator config parameters +type Config struct { + // Algo + Algorithm Algorithm + // MaxLength sets the maximum length for a generated password + MaxLength int64 + // MinLength sets the minimum length for a generated password + MinLength int64 + // NumberPass sets the number of passwords that are generated + // and returned by the generator + NumberPass int64 +} + +// Option is a function that can override default Config settings +type Option func(*Config) + +// NewConfig creates a new Config instance and pre-fills it with sane +// default settings. The Config is returned as pointer value +func NewConfig(o ...Option) *Config { + c := &Config{ + MaxLength: DefaultMaxLength, + MinLength: DefaultMinLength, + NumberPass: DefaultNumberPass, + } + + // Override defaults with optionally provided config.Option functions + for _, co := range o { + if co == nil { + continue + } + co(c) + } + return c +} + +// WithMinLength overrides the minimum password length +func WithMinLength(l int64) Option { + return func(c *Config) { + c.MinLength = l + } +} + +// WithMaxLength overrides the maximum password length +func WithMaxLength(l int64) Option { + return func(c *Config) { + c.MaxLength = l + } +} + +// WithNumberPass overrides the number of generated passwords setting +func WithNumberPass(n int64) Option { + return func(c *Config) { + c.NumberPass = n + } +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..71706ee --- /dev/null +++ b/config_test.go @@ -0,0 +1,69 @@ +package apg + +import ( + "testing" +) + +func TestNewConfig(t *testing.T) { + c := NewConfig() + if c == nil { + t.Errorf("NewConfig() failed, expected config pointer but got nil") + return + } + c = NewConfig(nil) + if c == nil { + t.Errorf("NewConfig() failed, expected config pointer but got nil") + return + } + if c.MinLength != DefaultMinLength { + t.Errorf("NewConfig() failed, expected min length: %d, got: %d", DefaultMinLength, + c.MinLength) + } + if c.MaxLength != DefaultMaxLength { + t.Errorf("NewConfig() failed, expected max length: %d, got: %d", DefaultMaxLength, + c.MaxLength) + } + if c.NumberPass != DefaultNumberPass { + t.Errorf("NewConfig() failed, expected number of passwords: %d, got: %d", + DefaultNumberPass, c.NumberPass) + } +} + +func TestWithMaxLength(t *testing.T) { + var e int64 = 123 + c := NewConfig(WithMaxLength(e)) + if c == nil { + t.Errorf("NewConfig(WithMaxLength()) failed, expected config pointer but got nil") + return + } + if c.MaxLength != e { + t.Errorf("NewConfig(WithMaxLength()) failed, expected max length: %d, got: %d", + e, c.MaxLength) + } +} + +func TestWithMinLength(t *testing.T) { + var e int64 = 1 + c := NewConfig(WithMinLength(e)) + if c == nil { + t.Errorf("NewConfig(WithMinLength()) failed, expected config pointer but got nil") + return + } + if c.MinLength != e { + t.Errorf("NewConfig(WithMinLength()) failed, expected min length: %d, got: %d", + e, c.MinLength) + } +} + +func TestWithNumberPass(t *testing.T) { + var e int64 = 123 + c := NewConfig(WithNumberPass(e)) + if c == nil { + t.Errorf("NewConfig(WithNumberPass()) failed, expected config pointer but got nil") + return + } + if c.NumberPass != e { + t.Errorf("NewConfig(WithNumberPass()) failed, expected number of passwords: %d, got: %d", + e, c.NumberPass) + } +} diff --git a/mode.go b/mode.go new file mode 100644 index 0000000..59ef878 --- /dev/null +++ b/mode.go @@ -0,0 +1,51 @@ +package apg + +// Mode represents a mode of characters +type Mode uint8 + +const ( + // ModeNumber sets the bitmask to include numbers in the generated passwords + ModeNumber = 1 << iota + // ModeLowerCase sets the bitmask to include lower case characters in the + // generated passwords + ModeLowerCase + // ModeUpperCase sets the bitmask to include upper case characters in the + // generated passwords + ModeUpperCase + // ModeSpecial sets the bitmask to include special characters in the + // generated passwords + ModeSpecial + // ModeHumanReadable sets the bitmask to generate human readable passwords + ModeHumanReadable +) + +const ( + // CharRangeAlphaLower represents all lower-case alphabetical characters + CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz" + // CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters + CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz" + // CharRangeAlphaUpper represents all upper-case alphabetical characters + CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + // CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters + CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" + // CharRangeNumber represents all numerical characters + CharRangeNumber = "1234567890" + // CharRangeNumberHuman represents all human-readable numerical characters + CharRangeNumberHuman = "23456789" + // CharRangeSpecial represents all special characters + CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` + // CharRangeSpecialHuman represents all human-readable special characters + CharRangeSpecialHuman = `#%*+-:;=` +) + +// SetMode sets a specific Mode to a given Mode bitmask +func SetMode(b, m Mode) Mode { return b | m } + +// ClearMode clears a specific Mode from a given Mode bitmask +func ClearMode(b, m Mode) Mode { return b &^ m } + +// ToggleMode toggles a specific Mode in a given Mode bitmask +func ToggleMode(b, m Mode) Mode { return b ^ m } + +// HasMode returns true if a given Mode bitmask holds a specific Mode +func HasMode(b, m Mode) bool { return b&m != 0 } diff --git a/mode_test.go b/mode_test.go new file mode 100644 index 0000000..80fa79d --- /dev/null +++ b/mode_test.go @@ -0,0 +1,40 @@ +package apg + +import ( + "testing" +) + +func TestSetClearHasToggleMode(t *testing.T) { + tt := []struct { + name string + mode Mode + }{ + {"ModeNumber", ModeNumber}, + {"ModeLowerCase", ModeLowerCase}, + {"ModeUpperCase", ModeUpperCase}, + {"ModeSpecial", ModeSpecial}, + {"ModeHumanReadable", ModeHumanReadable}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + var m Mode + m = SetMode(m, tc.mode) + if !HasMode(m, tc.mode) { + t.Errorf("SetMode() failed, mode not found in bitmask") + } + m = ToggleMode(m, tc.mode) + if HasMode(m, tc.mode) { + t.Errorf("ToggleMode() failed, mode found in bitmask") + } + m = ToggleMode(m, tc.mode) + if !HasMode(m, tc.mode) { + t.Errorf("ToggleMode() failed, mode not found in bitmask") + } + m = ClearMode(m, tc.mode) + if HasMode(m, tc.mode) { + t.Errorf("ClearMode() failed, mode found in bitmask") + } + }) + } +} diff --git a/random.go b/random.go index 2b84deb..12f792c 100644 --- a/random.go +++ b/random.go @@ -9,25 +9,6 @@ import ( "strings" ) -const ( - // CharRangeAlphaLower represents all lower-case alphabetical characters - CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz" - // CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters - CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz" - // CharRangeAlphaUpper represents all upper-case alphabetical characters - CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - // CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters - CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ" - // CharRangeNumber represents all numerical characters - CharRangeNumber = "1234567890" - // CharRangeNumberHuman represents all human-readable numerical characters - CharRangeNumberHuman = "23456789" - // CharRangeSpecial represents all special characters - CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~` - // CharRangeSpecialHuman represents all human-readable special characters - CharRangeSpecialHuman = `#%*+-:;=` -) - const ( // 7 bits to represent a letter index letterIdxBits = 7 @@ -64,10 +45,10 @@ func (g *Generator) RandomBytes(n int64) ([]byte, error) { return b, nil } -// RandomString returns a random string of length l based of the range of characters given. +// 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) RandomString(l int, cr string) (string, error) { +func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) { if l < 1 { return "", ErrInvalidLength } @@ -85,7 +66,7 @@ func (g *Generator) RandomString(l int, cr string) (string, error) { } for i, c, r := l-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; { if r == 0 { - _, err := rand.Read(rp) + _, err = rand.Read(rp) if err != nil { return rs.String(), err } diff --git a/random_test.go b/random_test.go index 651aeaa..f7d0bca 100644 --- a/random_test.go +++ b/random_test.go @@ -7,7 +7,7 @@ import ( ) func TestGenerator_CoinFlip(t *testing.T) { - g := New() + g := New(NewConfig()) cf := g.CoinFlip() if cf < 0 || cf > 1 { t.Errorf("CoinFlip failed(), expected 0 or 1, got: %d", cf) @@ -15,7 +15,7 @@ func TestGenerator_CoinFlip(t *testing.T) { } func TestGenerator_CoinFlipBool(t *testing.T) { - g := New() + g := New(NewConfig()) gt := false for i := 0; i < 500_000; i++ { cf := g.CoinFlipBool() @@ -43,7 +43,7 @@ func TestGenerator_RandNum(t *testing.T) { {"RandNum should fail on negative", -1, 0, 0, true}, } - g := New() + g := New(NewConfig()) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { rn, err := g.RandNum(tc.v) @@ -78,7 +78,7 @@ func TestGenerator_RandomBytes(t *testing.T) { {"-1 bytes of randomness", -1, true}, } - g := New() + g := New(NewConfig()) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { rb, err := g.RandomBytes(tc.l) @@ -102,7 +102,7 @@ func TestGenerator_RandomBytes(t *testing.T) { } func TestGenerator_RandomString(t *testing.T) { - g := New() + g := New(NewConfig()) l := 32 tt := []struct { name string @@ -133,15 +133,15 @@ func TestGenerator_RandomString(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - rs, err := g.RandomString(l, tc.cr) + rs, err := g.RandomStringFromCharRange(l, tc.cr) if err != nil && !tc.sf { - t.Errorf("RandomString failed: %s", err) + t.Errorf("RandomStringFromCharRange failed: %s", err) } if len(rs) != l && !tc.sf { - t.Errorf("RandomString failed. Expected length: %d, got: %d", l, len(rs)) + t.Errorf("RandomStringFromCharRange failed. Expected length: %d, got: %d", l, len(rs)) } if strings.ContainsAny(rs, tc.nr) { - t.Errorf("RandomString failed. Unexpected character found in returned string: %s", rs) + t.Errorf("RandomStringFromCharRange failed. Unexpected character found in returned string: %s", rs) } }) } @@ -149,7 +149,7 @@ func TestGenerator_RandomString(t *testing.T) { func BenchmarkGenerator_CoinFlip(b *testing.B) { b.ReportAllocs() - g := New() + g := New(NewConfig()) for i := 0; i < b.N; i++ { _ = g.CoinFlip() } @@ -157,7 +157,7 @@ func BenchmarkGenerator_CoinFlip(b *testing.B) { func BenchmarkGenerator_RandomBytes(b *testing.B) { b.ReportAllocs() - g := New() + g := New(NewConfig()) var l int64 = 1024 for i := 0; i < b.N; i++ { _, err := g.RandomBytes(l) @@ -170,12 +170,12 @@ func BenchmarkGenerator_RandomBytes(b *testing.B) { func BenchmarkGenerator_RandomString(b *testing.B) { b.ReportAllocs() - g := New() + g := New(NewConfig()) cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial for i := 0; i < b.N; i++ { - _, err := g.RandomString(32, cr) + _, err := g.RandomStringFromCharRange(32, cr) if err != nil { - b.Errorf("RandomString() failed: %s", err) + b.Errorf("RandomStringFromCharRange() failed: %s", err) } } }