mirror of
https://github.com/wneessen/apg-go.git
synced 2024-11-22 13:50:49 +01:00
Merge pull request #2 from wneessen/dev
v0.2.4: Added spelling support and new-style pw params
This commit is contained in:
commit
5ca140393d
8 changed files with 403 additions and 99 deletions
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
|
@ -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 ./...
|
||||
|
|
2
Makefile
2
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:
|
||||
|
|
121
apg.go
121
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
|
||||
}
|
||||
|
|
81
apg_test.go
81
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 {
|
||||
|
|
37
chars.go
Normal file
37
chars.go
Normal 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
110
convert.go
Normal 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
97
params.go
Normal 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
50
rand.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue