Merge pull request #2 from wneessen/dev

v0.2.4: Added spelling support and new-style pw params
This commit is contained in:
Winni Neessen 2021-03-21 15:32:14 +01:00 committed by GitHub
commit 5ca140393d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 403 additions and 99 deletions

View file

@ -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 ./...

View file

@ -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:

119
apg.go
View file

@ -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)
}
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
}
}
// 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
}
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
}

View file

@ -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 {

37
chars.go Normal file
View file

@ -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
}

110
convert.go Normal file
View file

@ -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
}

97
params.go Normal file
View file

@ -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)
}
}
}

50
rand.go Normal file
View file

@ -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
}