diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 89d6baa..d4195cc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -18,7 +18,7 @@ jobs: go-version: 1.15 - name: Build - run: go build -v ./apg.go + run: go build -v ./... - name: Test - run: go test -v + run: go test -v ./... diff --git a/Makefile b/Makefile index b25c7ee..aad01a3 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ clean: # @HELP removes built binaries and temporary files clean: bin-clean bin-clean: - rm -rf .go bin + rm -rf .go bin *.exe help: # @HELP prints this message help: diff --git a/apg.go b/apg.go index 057a444..b10f48c 100644 --- a/apg.go +++ b/apg.go @@ -1,17 +1,14 @@ package main import ( - "crypto/rand" "flag" "fmt" - "math/big" "os" - "regexp" ) // Constants const DefaultPwLenght int = 20 -const VersionString string = "0.2.3" +const VersionString string = "0.2.4" const PwLowerCharsHuman string = "abcdefghjkmnpqrstuvwxyz" const PwUpperCharsHuman string = "ABCDEFGHJKMNPQRSTUVWXYZ" const PwLowerChars string = "abcdefghijklmnopqrstuvwxyz" @@ -32,8 +29,11 @@ type cliOpts struct { useSpecial bool humanReadable bool excludeChars string + newStyleModes string + spellPassword bool showHelp bool showVersion bool + outputMode int } var config cliOpts @@ -45,7 +45,8 @@ func init() { flag.BoolVar(&config.useUpperCase, "U", false, "Use upper case characters in passwords") flag.BoolVar(&config.useNumber, "N", false, "Use numbers in passwords") flag.BoolVar(&config.useSpecial, "S", false, "Use special characters in passwords") - flag.BoolVar(&config.useComplex, "C", true, "Generate complex passwords (implies -L -U -N -S, disables -H)") + flag.BoolVar(&config.useComplex, "C", false, "Generate complex passwords (implies -L -U -N -S, disables -H)") + flag.BoolVar(&config.spellPassword, "l", false, "Spell generated password") flag.BoolVar(&config.humanReadable, "H", false, "Generate human-readable passwords") flag.BoolVar(&config.showVersion, "v", false, "Show version") @@ -56,6 +57,8 @@ func init() { // String flags flag.StringVar(&config.excludeChars, "E", "", "Exclude list of characters from generated password") + flag.StringVar(&config.newStyleModes, "M", "", + "New style password parameters (higher priority than single parameters)") flag.Parse() if config.showVersion { @@ -66,21 +69,8 @@ func init() { // Main function that generated the passwords and returns them func main() { - pwLength := config.minPassLen - if pwLength < config.minPassLen { - pwLength = config.minPassLen - } - if pwLength > config.maxPassLen { - pwLength = config.maxPassLen - } - if config.useComplex { - config.useUpperCase = true - config.useLowerCase = true - config.useSpecial = true - config.useNumber = true - config.humanReadable = false - } - + parseParams() + pwLength := getPwLengthFromParams() charRange := getCharRange() for i := 1; i <= config.numOfPass; i++ { @@ -89,83 +79,22 @@ func main() { 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 - pwSpecialChars := PwSpecialChars - if config.humanReadable { - pwUpperChars = PwUpperCharsHuman - pwLowerChars = PwLowerCharsHuman - pwNumbers = PwNumbersHuman - pwSpecialChars = PwSpecialCharsHuman - } - - var charRange string - if config.useLowerCase { - charRange = charRange + pwLowerChars - } - if config.useUpperCase { - charRange = charRange + pwUpperChars - } - if config.useNumber { - charRange = charRange + pwNumbers - } - if config.useSpecial { - charRange = charRange + pwSpecialChars - } - if config.excludeChars != "" { - regExp := regexp.MustCompile("[" + config.excludeChars + "]") - charRange = regExp.ReplaceAllLiteralString(charRange, "") - } - - return charRange -} - -// 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, err := getRandNum(availCharsLength) - if err != nil { - return "", err + switch config.outputMode { + case 1: + { + spelledPw, err := spellPasswordString(pwString) + if err != nil { + fmt.Printf("spellPasswordString returned an error: %q\n", err.Error()) + os.Exit(1) + } + fmt.Printf("%v (%v)\n", pwString, spelledPw) + } + default: + { + fmt.Println(pwString) + break + } } - returnString[i] = charSlice[randNum] } - return string(returnString), nil -} - -// 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 { - return 0, err - } - randNum := int(randNum64.Int64()) - 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 779e3a1..ad9f0a0 100644 --- a/apg_test.go +++ b/apg_test.go @@ -207,6 +207,72 @@ func TestGetCharRange(t *testing.T) { }) } +// Test Conversions +func TestConvert(t *testing.T) { + t.Run("convert_A_to_Alfa", func(t *testing.T) { + charToString, err := convertCharToName('A') + if err != nil { + t.Errorf("Character to string conversion failed: %v", err.Error()) + } + if charToString != "Alfa" { + t.Errorf("Converting 'A' to string did not return the correct value of 'Alfa': %q", charToString) + } + }) + t.Run("convert_a_to_alfa", func(t *testing.T) { + charToString, err := convertCharToName('a') + if err != nil { + t.Errorf("Character to string conversion failed: %v", err.Error()) + } + if charToString != "alfa" { + t.Errorf("Converting 'a' to string did not return the correct value of 'alfa': %q", charToString) + } + }) + t.Run("convert_0_to_ZERO", func(t *testing.T) { + charToString, err := convertCharToName('0') + if err != nil { + t.Errorf("Character to string conversion failed: %v", err.Error()) + } + if charToString != "ZERO" { + t.Errorf("Converting '0' to string did not return the correct value of 'ZERO': %q", charToString) + } + }) + t.Run("convert_/_to_SLASH", func(t *testing.T) { + charToString, err := convertCharToName('/') + if err != nil { + t.Errorf("Character to string conversion failed: %v", err.Error()) + } + if charToString != "SLASH" { + t.Errorf("Converting '/' to string did not return the correct value of 'SLASH': %q", charToString) + } + }) + t.Run("all_chars_convert_to_string", func(t *testing.T) { + config.useUpperCase = true + config.useLowerCase = true + config.useNumber = true + config.useSpecial = true + config.humanReadable = false + charRange := getCharRange() + for _, curChar := range charRange { + _, err := convertCharToName(byte(curChar)) + if err != nil { + t.Errorf("Character to string conversion failed: %v", err.Error()) + } + } + }) + t.Run("spell_Ab!_to_strings", func(t *testing.T) { + pwString := "Ab!" + spelledString, err := spellPasswordString(pwString) + if err != nil { + t.Errorf("password spelling failed: %v", err.Error()) + } + if spelledString != "Alfa/bravo/EXCLAMATION_POINT" { + t.Errorf( + "Spelling pwString 'Ab!' is expected to provide 'Alfa/bravo/EXCLAMATION_POINT', but returned: %q", + spelledString) + } + }) +} + // Forced failures func TestForceFailures(t *testing.T) { t.Run("too_big_big.NewInt_value", func(t *testing.T) { @@ -241,6 +307,21 @@ func BenchmarkGetRandChar(b *testing.B) { } } +// Benchmark: Random char generation +func BenchmarkConvertChar(b *testing.B) { + config.useUpperCase = true + config.useLowerCase = true + config.useNumber = true + config.useSpecial = true + config.humanReadable = false + charRange := getCharRange() + for i := 0; i < b.N; i++ { + charToConv, _ := getRandChar(&charRange, 1) + charBytes := []byte(charToConv) + _, _ = convertCharToName(charBytes[0]) + } +} + // Contains function to search a given slice for values func containsByte(allowedBytes []int, currentChar int) bool { for _, charInt := range allowedBytes { diff --git a/chars.go b/chars.go new file mode 100644 index 0000000..717d29e --- /dev/null +++ b/chars.go @@ -0,0 +1,37 @@ +package main + +import "regexp" + +// Provide the range of available characters based on provided parameters +func getCharRange() string { + pwUpperChars := PwUpperChars + pwLowerChars := PwLowerChars + pwNumbers := PwNumbers + pwSpecialChars := PwSpecialChars + if config.humanReadable { + pwUpperChars = PwUpperCharsHuman + pwLowerChars = PwLowerCharsHuman + pwNumbers = PwNumbersHuman + pwSpecialChars = PwSpecialCharsHuman + } + + var charRange string + if config.useLowerCase { + charRange = charRange + pwLowerChars + } + if config.useUpperCase { + charRange = charRange + pwUpperChars + } + if config.useNumber { + charRange = charRange + pwNumbers + } + if config.useSpecial { + charRange = charRange + pwSpecialChars + } + if config.excludeChars != "" { + regExp := regexp.MustCompile("[" + config.excludeChars + "]") + charRange = regExp.ReplaceAllLiteralString(charRange, "") + } + + return charRange +} diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..c9436fe --- /dev/null +++ b/convert.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "strings" +) + +var ( + symbNumNames = map[byte]string{ + '1': "ONE", + '2': "TWO", + '3': "THREE", + '4': "FOUR", + '5': "FIVE", + '6': "SIX", + '7': "SEVEN", + '8': "EIGHT", + '9': "NINE", + '0': "ZERO", + 33: "EXCLAMATION_POINT", + 34: "QUOTATION_MARK", + 35: "CROSSHATCH", + 36: "DOLLAR_SIGN", + 37: "PERCENT_SIGN", + 38: "AMPERSAND", + 39: "APOSTROPHE", + 40: "LEFT_PARENTHESIS", + 41: "RIGHT_PARENTHESIS", + 42: "ASTERISK", + 43: "PLUS_SIGN", + 44: "COMMA", + 45: "HYPHEN", + 46: "PERIOD", + 47: "SLASH", + 58: "COLON", + 59: "SEMICOLON", + 60: "LESS_THAN", + 61: "EQUAL_SIGN", + 62: "GREATER_THAN", + 63: "QUESTION_MARK", + 64: "AT_SIGN", + 91: "LEFT_BRACKET", + 92: "BACKSLASH", + 93: "RIGHT_BRACKET", + 94: "CIRCUMFLEX", + 95: "UNDERSCORE", + 96: "GRAVE", + 123: "LEFT_BRACE", + 124: "VERTICAL_BAR", + 125: "RIGHT_BRACE", + 126: "TILDE", + } + alphabetNames = map[byte]string{ + 'A': "Alfa", + 'B': "Bravo", + 'C': "Charlie", + 'D': "Delta", + 'E': "Echo", + 'F': "Foxtrot", + 'G': "Golf", + 'H': "Hotel", + 'I': "India", + 'J': "Juliett", + 'K': "Kilo", + 'L': "Lima", + 'M': "Mike", + 'N': "November", + 'O': "Oscar", + 'P': "Papa", + 'Q': "Quebec", + 'R': "Romeo", + 'S': "Sierra", + 'T': "Tango", + 'U': "Uniform", + 'V': "Victor", + 'W': "Whiskey", + 'X': "X_ray", + 'Y': "Yankee", + 'Z': "Zulu", + } +) + +func spellPasswordString(pwString string) (string, error) { + var returnString []string + for _, curChar := range pwString { + curSpellString, err := convertCharToName(byte(curChar)) + if err != nil { + return "", err + } + returnString = append(returnString, curSpellString) + } + return strings.Join(returnString, "/"), nil +} + +func convertCharToName(charByte byte) (string, error) { + var returnString string + if charByte > 64 && charByte < 91 { + returnString = alphabetNames[charByte] + } else if charByte > 96 && charByte < 123 { + returnString = strings.ToLower(alphabetNames[charByte-32]) + } else { + returnString = symbNumNames[charByte] + } + + if returnString == "" { + err := fmt.Errorf("cannot convert to character to name: %q is an unknown character", charByte) + return "", err + } + return returnString, nil +} diff --git a/params.go b/params.go new file mode 100644 index 0000000..48b2ae0 --- /dev/null +++ b/params.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "os" +) + +// Parse the parameters and set the according config flags +func parseParams() { + parseNewStyleParams() + + // Complex overrides everything + if config.useComplex { + config.useUpperCase = true + config.useLowerCase = true + config.useSpecial = true + config.useNumber = true + config.humanReadable = false + } + + if config.useUpperCase == false && + config.useLowerCase == false && + config.useNumber == false && + config.useSpecial == false { + fmt.Printf("No password mode set. Cannot generate password from empty character set.") + os.Exit(1) + } + + // Set output mode + if config.spellPassword { + config.outputMode = 1 + } +} + +// Get the password length from the given cli flags +func getPwLengthFromParams() int { + pwLength := config.minPassLen + if pwLength < config.minPassLen { + pwLength = config.minPassLen + } + if pwLength > config.maxPassLen { + pwLength = config.maxPassLen + } + + return pwLength +} + +// Parse the new style parameters +func parseNewStyleParams() { + if config.newStyleModes == "" { + return + } + + for _, curParam := range config.newStyleModes { + switch curParam { + case 'S': + config.useSpecial = true + break + case 's': + config.useSpecial = false + break + case 'N': + config.useNumber = true + break + case 'n': + config.useNumber = false + break + case 'L': + config.useLowerCase = true + break + case 'l': + config.useLowerCase = false + break + case 'U': + config.useUpperCase = true + break + case 'u': + config.useUpperCase = false + break + case 'H': + config.humanReadable = true + break + case 'h': + config.humanReadable = false + break + case 'C': + config.useComplex = true + break + case 'c': + config.useComplex = false + break + default: + fmt.Printf("Unknown password style parameter: %q\n", string(curParam)) + os.Exit(1) + } + } +} diff --git a/rand.go b/rand.go new file mode 100644 index 0000000..7c12cd7 --- /dev/null +++ b/rand.go @@ -0,0 +1,50 @@ +package main + +import ( + "crypto/rand" + "fmt" + "math/big" +) + +// 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, err := getRandNum(availCharsLength) + if err != nil { + return "", err + } + returnString[i] = charSlice[randNum] + } + return string(returnString), nil +} + +// 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 { + return 0, err + } + randNum := int(randNum64.Int64()) + if randNum < 0 { + err := fmt.Errorf("generated random number does not fit as int64: %v", randNum64) + return 0, err + } + return randNum, nil +}