From 46e47348e28ef2c222c7ae3c1b7cb9e9ed9b2a22 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 21 Mar 2021 13:25:52 +0100 Subject: [PATCH] v0.2.3: More chars, cleanup and better test coverage - Added more special characters - Fixed issue with ` character - Added lots of tests - Moved character range generation into it's own function - Better error handling - A bit of code cleanup --- apg.go | 65 ++++++++++---- apg_test.go | 254 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 272 insertions(+), 47 deletions(-) diff --git a/apg.go b/apg.go index 8595d2a..057a444 100644 --- a/apg.go +++ b/apg.go @@ -11,13 +11,13 @@ import ( // Constants const DefaultPwLenght int = 20 -const VersionString string = "0.2.2" +const VersionString string = "0.2.3" const PwLowerCharsHuman string = "abcdefghjkmnpqrstuvwxyz" const PwUpperCharsHuman string = "ABCDEFGHJKMNPQRSTUVWXYZ" const PwLowerChars string = "abcdefghijklmnopqrstuvwxyz" const PwUpperChars string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -const PwSpecialCharsHuman string = "\"#/\\$%&+-*" -const PwSpecialChars string = "\"#/!\\$%&+-*.,?=()[]{}:;~^|" +const PwSpecialCharsHuman string = "\"#%*+-/:;=\\_|~" +const PwSpecialChars string = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" const PwNumbersHuman string = "23456789" const PwNumbers string = "1234567890" @@ -59,11 +59,12 @@ func init() { flag.Parse() if config.showVersion { - _, _ = os.Stderr.WriteString("Advanced Password Generator v" + VersionString + "\n") + _, _ = os.Stderr.WriteString("Winni's Advanced Password Generator Clone (apg.go) v" + VersionString + "\n") os.Exit(0) } } +// Main function that generated the passwords and returns them func main() { pwLength := config.minPassLen if pwLength < config.minPassLen { @@ -80,6 +81,20 @@ func main() { config.humanReadable = false } + charRange := getCharRange() + + for i := 1; i <= config.numOfPass; i++ { + pwString, err := getRandChar(&charRange, pwLength) + if err != nil { + fmt.Printf("getRandChar returned an error: %q\n", err.Error()) + os.Exit(1) + } + fmt.Println(pwString) + } +} + +// Provide the range of available characters based on provided parameters +func getCharRange() string { pwUpperChars := PwUpperChars pwLowerChars := PwLowerChars pwNumbers := PwNumbers @@ -109,30 +124,48 @@ func main() { charRange = regExp.ReplaceAllLiteralString(charRange, "") } - for i := 1; i <= config.numOfPass; i++ { - pwString := getRandChar(&charRange, pwLength) - fmt.Println(pwString) - } + return charRange } -func getRandChar(charRange *string, pwLength int) string { +// Generate random characters based on given character range +// and password length +func getRandChar(charRange *string, pwLength int) (string, error) { + if pwLength <= 0 { + err := fmt.Errorf("provided pwLength value is <= 0: %v", pwLength) + return "", err + } availCharsLength := len(*charRange) charSlice := []byte(*charRange) returnString := make([]byte, pwLength) for i := 0; i < pwLength; i++ { - randNum := getRandNum(availCharsLength) - returnString = append(returnString, charSlice[randNum]) + randNum, err := getRandNum(availCharsLength) + if err != nil { + return "", err + } + returnString[i] = charSlice[randNum] } - return string(returnString) + return string(returnString), nil } -func getRandNum(maxNum int) int { +// Generate a random number with given maximum value +func getRandNum(maxNum int) (int, error) { + if maxNum <= 0 { + err := fmt.Errorf("provided maxNum is <= 0: %v", maxNum) + return 0, err + } maxNumBigInt := big.NewInt(int64(maxNum)) + if !maxNumBigInt.IsUint64() { + err := fmt.Errorf("big.NewInt() generation returned negative value: %v", maxNumBigInt) + return 0, err + } randNum64, err := rand.Int(rand.Reader, maxNumBigInt) if err != nil { - fmt.Printf("An error occured while generating random number: %v", err) - os.Exit(1) + return 0, err } randNum := int(randNum64.Int64()) - return randNum + if randNum < 0 { + err := fmt.Errorf("generated random number does not fit as int64: %v", randNum64) + return 0, err + } + return randNum, nil } diff --git a/apg_test.go b/apg_test.go index 23a7fed..779e3a1 100644 --- a/apg_test.go +++ b/apg_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "math/big" + "testing" +) // Make sure the flags are initalized var _ = func() bool { @@ -9,45 +12,224 @@ var _ = func() bool { }() // Test getRandNum with max 1000 -func TestGetRandNumMax1000(t *testing.T) { - randNum := getRandNum(1000) - if randNum > 1000 { - t.Errorf("Generated random number between 0 and 1000 is too big: %d", randNum) - } - if randNum < 0 { - t.Errorf("Generated random number between 0 and 1000 is too small: %d", randNum) - } -} - -// Test getRandNum with max 1 -func TestGetRandNumMax1(t *testing.T) { - randNum := getRandNum(1) - if randNum > 1 { - t.Errorf("Generated random number between 0 and 1 is too big: %d", randNum) - } - if randNum < 0 { - t.Errorf("Generated random number between 0 and 1 is too small: %d", randNum) - } +func TestGetRandNum(t *testing.T) { + t.Run("maxNum_is_1000", func(t *testing.T) { + randNum, err := getRandNum(1000) + if err != nil { + t.Errorf("Random number generation failed: %v", err.Error()) + } + if randNum > 1000 { + t.Errorf("Generated random number between 0 and 1000 is too big: %v", randNum) + } + if randNum < 0 { + t.Errorf("Generated random number between 0 and 1000 is too small: %v", randNum) + } + }) + t.Run("maxNum_is_1", func(t *testing.T) { + randNum, err := getRandNum(1) + if err != nil { + t.Errorf("Random number generation failed: %v", err.Error()) + } + if randNum > 1 { + t.Errorf("Generated random number between 0 and 1000 is too big: %v", randNum) + } + if randNum < 0 { + t.Errorf("Generated random number between 0 and 1000 is too small: %v", randNum) + } + }) + t.Run("maxNum_is_0", func(t *testing.T) { + randNum, err := getRandNum(0) + if err == nil { + t.Errorf("Random number expected to fail, but provided a value instead: %v", randNum) + } + }) } // Test getRandChar func TestGetRandChar(t *testing.T) { - charRange := "ABC" - randChar := getRandChar(&charRange, 1) - if randChar != "A" && randChar != "B" && randChar != "C" { - t.Errorf("Random character generation failed. Expected A, B or C but got: %v", randChar) - } + t.Run("return_value_is_A_B_or_C", func(t *testing.T) { + charRange := "ABC" + randChar, err := getRandChar(&charRange, 1) + if err != nil { + t.Errorf("Random character generation failed => %v", err.Error()) + } + if randChar != "A" && randChar != "B" && randChar != "C" { + t.Errorf("Random character generation failed. Expected A, B or C but got: %v", randChar) + } + }) - randChar = getRandChar(&charRange, 1000) - if len(randChar) != 1000 { - t.Errorf("Generated random characters with 1000 chars returned wrong amount of chars: %v", len(randChar)) - } + t.Run("return_value_has_specific_length", func(t *testing.T) { + charRange := "ABC" + randChar, err := getRandChar(&charRange, 1000) + if err != nil { + t.Errorf("Random character generation failed => %v", err.Error()) + } + if len(randChar) != 1000 { + t.Errorf("Generated random characters with 1000 chars returned wrong amount of chars: %v", + len(randChar)) + } + }) + + t.Run("fail", func(t *testing.T) { + charRange := "ABC" + randChar, err := getRandChar(&charRange, -2000) + if err == nil { + t.Errorf("Generated random characters expected to fail, but returned a value => %v", + randChar) + } + }) +} + +// Test getCharRange() with different config settings +func TestGetCharRange(t *testing.T) { + + t.Run("lower_case_only", func(t *testing.T) { + // Lower case only + allowedBytes := []int{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'} + config.useLowerCase = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for lower-case only returned invalid value: %v", + string(curChar)) + } + } + }) + + // Lower case only (human readable) + t.Run("lower_case_only_human_readable", func(t *testing.T) { + allowedBytes := []int{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'} + config.humanReadable = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for lower-case only (human readable) returned invalid value: %v", + string(curChar)) + } + } + }) + + // Upper case only + t.Run("upper_case_only", func(t *testing.T) { + allowedBytes := []int{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'} + config.humanReadable = false + config.useLowerCase = false + config.useUpperCase = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for upper-case only returned invalid value: %v", + string(curChar)) + } + } + }) + + // Upper case only (human readable) + t.Run("upper_case_only_human_readable", func(t *testing.T) { + allowedBytes := []int{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', + 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'} + config.humanReadable = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for upper-case only (human readable) returned invalid value: %v", + string(curChar)) + } + } + }) + + // Numbers only + t.Run("numbers_only", func(t *testing.T) { + allowedBytes := []int{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} + config.humanReadable = false + config.useUpperCase = false + config.useNumber = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for numbers only returned invalid value: %v", + string(curChar)) + } + } + }) + + // Numbers only (human readable) + t.Run("numbers_only_human_readable", func(t *testing.T) { + allowedBytes := []int{'2', '3', '4', '5', '6', '7', '8', '9'} + config.humanReadable = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for numbers (human readable) only returned invalid value: %v", + string(curChar)) + } + } + }) + + // Special characters only + t.Run("special_chars_only", func(t *testing.T) { + allowedBytes := []int{'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', + ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'} + config.humanReadable = false + config.useNumber = false + config.useSpecial = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for special characters only returned invalid value: %v", + string(curChar)) + } + } + }) + + // Special characters only (human readable) + t.Run("special_chars_only_human_readable", func(t *testing.T) { + allowedBytes := []int{'"', '#', '%', '*', '+', '-', '/', ':', ';', '=', '\\', '_', '|', '~'} + config.humanReadable = true + charRange := getCharRange() + for _, curChar := range charRange { + searchAllowedBytes := containsByte(allowedBytes, int(curChar)) + if !searchAllowedBytes { + t.Errorf("Character range for special characters only returned invalid value: %v", + string(curChar)) + } + } + }) +} + +// Forced failures +func TestForceFailures(t *testing.T) { + t.Run("too_big_big.NewInt_value", func(t *testing.T) { + maxNum := 9223372036854775807 + maxNumBigInt := big.NewInt(int64(maxNum) + 1) + if maxNumBigInt.IsUint64() { + t.Errorf("Calling big.NewInt() with too large number expected to fail: %v", maxNumBigInt) + } + }) + + t.Run("negative value for big.NewInt()", func(t *testing.T) { + randNum, err := getRandNum(-20000) + if err == nil { + t.Errorf("Calling getRandNum() with negative value is expected to fail, but returned value: %v", + randNum) + } + }) } // Benchmark: Random number generation func BenchmarkGetRandNum(b *testing.B) { for i := 0; i < b.N; i++ { - getRandNum(100000) + _, _ = getRandNum(100000) } } @@ -55,6 +237,16 @@ func BenchmarkGetRandNum(b *testing.B) { func BenchmarkGetRandChar(b *testing.B) { charRange := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\"#/!\\$%&+-*.,?=()[]{}:;~^|" for i := 0; i < b.N; i++ { - getRandChar(&charRange, 20) + _, _ = getRandChar(&charRange, 20) } } + +// Contains function to search a given slice for values +func containsByte(allowedBytes []int, currentChar int) bool { + for _, charInt := range allowedBytes { + if charInt == currentChar { + return true + } + } + return false +}