mirror of
https://github.com/wneessen/apg-go.git
synced 2024-11-22 13:50:49 +01:00
Missing cmd/
This commit is contained in:
parent
6ebd86d00a
commit
7871b4307b
3 changed files with 387 additions and 4 deletions
|
@ -4,9 +4,7 @@
|
||||||
<option name="autoReloadType" value="ALL" />
|
<option name="autoReloadType" value="ALL" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="fbb0c733-4aa1-4d27-87d5-c7276d8aa613" name="Default Changelist" comment="v0.3.3: Separated HIBP code into its own module">
|
<list default="true" id="fbb0c733-4aa1-4d27-87d5-c7276d8aa613" name="Default Changelist" comment="Updated WS" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
@ -270,7 +268,8 @@
|
||||||
<MESSAGE value="Major refactor so that cmd and lib are separated" />
|
<MESSAGE value="Major refactor so that cmd and lib are separated" />
|
||||||
<MESSAGE value="Updated build files for Arch and OpenBSD" />
|
<MESSAGE value="Updated build files for Arch and OpenBSD" />
|
||||||
<MESSAGE value="v0.3.3: Separated HIBP code into its own module" />
|
<MESSAGE value="v0.3.3: Separated HIBP code into its own module" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="v0.3.3: Separated HIBP code into its own module" />
|
<MESSAGE value="Updated WS" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Updated WS" />
|
||||||
</component>
|
</component>
|
||||||
<component name="VgoProject">
|
<component name="VgoProject">
|
||||||
<integration-enabled>true</integration-enabled>
|
<integration-enabled>true</integration-enabled>
|
||||||
|
|
96
cmd/apg/apg.go
Normal file
96
cmd/apg/apg.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/wneessen/apg-go/chars"
|
||||||
|
"github.com/wneessen/apg-go/config"
|
||||||
|
"github.com/wneessen/apg-go/random"
|
||||||
|
"github.com/wneessen/apg-go/spelling"
|
||||||
|
hibp "github.com/wneessen/go-hibp"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const VersionString string = "0.3.3"
|
||||||
|
|
||||||
|
// Help text
|
||||||
|
const usage = `apg-go // A "Automated Password Generator"-clone
|
||||||
|
Copyright (c) 2021 Winni Neessen
|
||||||
|
|
||||||
|
apg [-m <length>] [-x <length>] [-L] [-U] [-N] [-S] [-H] [-C]
|
||||||
|
[-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-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)
|
||||||
|
'--> this feature requires internet connectivity
|
||||||
|
-h Show this help text
|
||||||
|
-v Show version string`
|
||||||
|
|
||||||
|
// Main function that generated the passwords and returns them
|
||||||
|
func main() {
|
||||||
|
// Log configuration
|
||||||
|
log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile)
|
||||||
|
|
||||||
|
// Read and parse flags
|
||||||
|
flag.Usage = func() { _, _ = fmt.Fprintf(os.Stderr, "%s\n", usage) }
|
||||||
|
var cfgObj = config.New()
|
||||||
|
|
||||||
|
// Show version and exit
|
||||||
|
if cfgObj.ShowVersion {
|
||||||
|
_, _ = os.Stderr.WriteString(`apg-go // A "Automated Password Generator"-clone v` + VersionString + "\n")
|
||||||
|
_, _ = os.Stderr.WriteString("(C) 2021 by Winni Neessen\n")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set PW length and available characterset
|
||||||
|
charRange := chars.GetRange(&cfgObj)
|
||||||
|
|
||||||
|
// Generate passwords
|
||||||
|
for i := 1; i <= cfgObj.NumOfPass; i++ {
|
||||||
|
pwLength := config.GetPwLengthFromParams(&cfgObj)
|
||||||
|
pwString, err := random.GetChar(&charRange, pwLength)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("getRandChar returned an error: %q\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cfgObj.OutputMode {
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
spelledPw, err := spelling.String(pwString)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("spellPasswordString returned an error: %q\n", err.Error())
|
||||||
|
}
|
||||||
|
fmt.Printf("%v (%v)\n", pwString, spelledPw)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
fmt.Println(pwString)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfgObj.CheckHibp {
|
||||||
|
isPwned, err := hibp.Check(pwString)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to check HIBP database: %v", err)
|
||||||
|
}
|
||||||
|
if isPwned {
|
||||||
|
fmt.Print("^-- !!WARNING: The previously generated password was found in HIPB database. Do not use it!!\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
288
cmd/apg/apg_test.go
Normal file
288
cmd/apg/apg_test.go
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wneessen/apg-go/chars"
|
||||||
|
"github.com/wneessen/apg-go/config"
|
||||||
|
"github.com/wneessen/apg-go/random"
|
||||||
|
"github.com/wneessen/apg-go/spelling"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cfgObj config.Config
|
||||||
|
|
||||||
|
// Make sure the flags are initalized
|
||||||
|
var _ = func() bool {
|
||||||
|
testing.Init()
|
||||||
|
cfgObj = config.New()
|
||||||
|
return true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Test getRandNum with max 1000
|
||||||
|
func TestGetRandNum(t *testing.T) {
|
||||||
|
testTable := []struct {
|
||||||
|
testName string
|
||||||
|
givenVal int
|
||||||
|
maxRet int
|
||||||
|
minRet int
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{"randNum up to 1000", 1000, 1000, 0, false},
|
||||||
|
{"randNum should be 1", 1, 1, 0, false},
|
||||||
|
{"randNum should fail on 0", 0, 0, 0, true},
|
||||||
|
{"randNum should fail on negative", -1, 0, 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testTable {
|
||||||
|
t.Run(testCase.testName, func(t *testing.T) {
|
||||||
|
randNum, err := random.GetNum(testCase.givenVal)
|
||||||
|
if testCase.shouldFail {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Random number generation succeeded but was expected to fail. Given: %v, returned: %v",
|
||||||
|
testCase.givenVal, randNum)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Random number generation failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
if randNum > testCase.maxRet {
|
||||||
|
t.Errorf("Random number generation returned too big value. Given %v, expected max: %v, got: %v",
|
||||||
|
testCase.givenVal, testCase.maxRet, randNum)
|
||||||
|
}
|
||||||
|
if randNum < testCase.minRet {
|
||||||
|
t.Errorf("Random number generation returned too small value. Given %v, expected max: %v, got: %v",
|
||||||
|
testCase.givenVal, testCase.minRet, randNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Pwlength
|
||||||
|
func TestGenLength(t *testing.T) {
|
||||||
|
testTable := []struct {
|
||||||
|
testName string
|
||||||
|
minLength int
|
||||||
|
maxLength int
|
||||||
|
}{
|
||||||
|
{"pwLength defaults", config.DefaultMinLength, config.DefaultMaxLength},
|
||||||
|
{"pwLength 0 to 1", 0, 1},
|
||||||
|
{"pwLength 1 to 10", 0, 10},
|
||||||
|
{"pwLength 10 to 100", 10, 100},
|
||||||
|
}
|
||||||
|
|
||||||
|
charRange := chars.GetRange(&cfgObj)
|
||||||
|
for _, testCase := range testTable {
|
||||||
|
t.Run(testCase.testName, func(t *testing.T) {
|
||||||
|
cfgObj.MinPassLen = testCase.minLength
|
||||||
|
cfgObj.MaxPassLen = testCase.maxLength
|
||||||
|
pwLength := config.GetPwLengthFromParams(&cfgObj)
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
pwString, err := random.GetChar(&charRange, pwLength)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("getRandChar returned an error: %q", err)
|
||||||
|
}
|
||||||
|
retLen := len(pwString)
|
||||||
|
if retLen > testCase.maxLength {
|
||||||
|
t.Errorf("Generated password length too long. GivenMin %v, GivenMax: %v, Returned length %v",
|
||||||
|
testCase.minLength, testCase.maxLength, retLen)
|
||||||
|
}
|
||||||
|
if retLen < testCase.minLength {
|
||||||
|
t.Errorf("Generated password length too short. GivenMin %v, GivenMax: %v, Returned length %v",
|
||||||
|
testCase.minLength, testCase.maxLength, retLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test getRandChar
|
||||||
|
func TestGetRandChar(t *testing.T) {
|
||||||
|
t.Run("return_value_is_A_B_or_C", func(t *testing.T) {
|
||||||
|
charRange := "ABC"
|
||||||
|
randChar, err := random.GetChar(&charRange, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Random character generation failed => %v", err.Error())
|
||||||
|
}
|
||||||
|
if randChar != "A" && randChar != "B" && randChar != "C" {
|
||||||
|
t.Fatalf("Random character generation failed. Expected A, B or C but got: %v", randChar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("return_value_has_specific_length", func(t *testing.T) {
|
||||||
|
charRange := "ABC"
|
||||||
|
randChar, err := random.GetChar(&charRange, 1000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Random character generation failed => %v", err.Error())
|
||||||
|
}
|
||||||
|
if len(randChar) != 1000 {
|
||||||
|
t.Fatalf("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 := random.GetChar(&charRange, -2000)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Generated random characters expected to fail, but returned a value => %v",
|
||||||
|
randChar)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test getCharRange() with different cfgObj settings
|
||||||
|
func TestGetCharRange(t *testing.T) {
|
||||||
|
lowerCaseBytes := []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'}
|
||||||
|
lowerCaseHumanBytes := []int{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r',
|
||||||
|
's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}
|
||||||
|
upperCaseBytes := []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'}
|
||||||
|
upperCaseHumanBytes := []int{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
|
||||||
|
'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}
|
||||||
|
numberBytes := []int{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
|
||||||
|
numberHumanBytes := []int{'2', '3', '4', '5', '6', '7', '8', '9'}
|
||||||
|
specialBytes := []int{'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':',
|
||||||
|
';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'}
|
||||||
|
specialHumanBytes := []int{'"', '#', '%', '*', '+', '-', '/', ':', ';', '=', '\\', '_', '|', '~'}
|
||||||
|
testTable := []struct {
|
||||||
|
testName string
|
||||||
|
allowedBytes []int
|
||||||
|
useLowerCase bool
|
||||||
|
useUpperCase bool
|
||||||
|
useNumber bool
|
||||||
|
useSpecial bool
|
||||||
|
humanReadable bool
|
||||||
|
}{
|
||||||
|
{"lowercase_only", lowerCaseBytes, true, false, false, false, false},
|
||||||
|
{"lowercase_only_human", lowerCaseHumanBytes, true, false, false, false, true},
|
||||||
|
{"uppercase_only", upperCaseBytes, false, true, false, false, false},
|
||||||
|
{"uppercase_only_human", upperCaseHumanBytes, false, true, false, false, true},
|
||||||
|
{"number_only", numberBytes, false, false, true, false, false},
|
||||||
|
{"number_only_human", numberHumanBytes, false, false, true, false, true},
|
||||||
|
{"special_only", specialBytes, false, false, false, true, false},
|
||||||
|
{"special_only_human", specialHumanBytes, false, false, false, true, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testTable {
|
||||||
|
t.Run(testCase.testName, func(t *testing.T) {
|
||||||
|
cfgObj.UseLowerCase = testCase.useLowerCase
|
||||||
|
cfgObj.UseUpperCase = testCase.useUpperCase
|
||||||
|
cfgObj.UseNumber = testCase.useNumber
|
||||||
|
cfgObj.UseSpecial = testCase.useSpecial
|
||||||
|
cfgObj.HumanReadable = testCase.humanReadable
|
||||||
|
charRange := chars.GetRange(&cfgObj)
|
||||||
|
for _, curChar := range charRange {
|
||||||
|
searchAllowedBytes := containsByte(testCase.allowedBytes, int(curChar), t)
|
||||||
|
if !searchAllowedBytes {
|
||||||
|
t.Errorf("Character range returned invalid value: %v", string(curChar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Conversions
|
||||||
|
func TestConvert(t *testing.T) {
|
||||||
|
testTable := []struct {
|
||||||
|
testName string
|
||||||
|
givenVal byte
|
||||||
|
expVal string
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{"convert_A_to_Alfa", 'A', "Alfa", false},
|
||||||
|
{"convert_a_to_alfa", 'a', "alfa", false},
|
||||||
|
{"convert_0_to_ZERO", '0', "ZERO", false},
|
||||||
|
{"convert_/_to_SLASH", '/', "SLASH", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testTable {
|
||||||
|
t.Run(testCase.testName, func(t *testing.T) {
|
||||||
|
charToString, err := spelling.ConvertCharToName(testCase.givenVal)
|
||||||
|
if testCase.shouldFail {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Character to string conversion succeeded but was expected to fail. Given: %v, returned: %v",
|
||||||
|
testCase.givenVal, charToString)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Character to string conversion failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
if charToString != testCase.expVal {
|
||||||
|
t.Errorf("Character to String conversion fail. Given: %q, expected: %q, got: %q",
|
||||||
|
testCase.givenVal, testCase.expVal, charToString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("all_chars_must_return_a_conversion_string", func(t *testing.T) {
|
||||||
|
cfgObj.UseUpperCase = true
|
||||||
|
cfgObj.UseLowerCase = true
|
||||||
|
cfgObj.UseNumber = true
|
||||||
|
cfgObj.UseSpecial = true
|
||||||
|
cfgObj.HumanReadable = false
|
||||||
|
charRange := chars.GetRange(&cfgObj)
|
||||||
|
for _, curChar := range charRange {
|
||||||
|
_, err := spelling.ConvertCharToName(byte(curChar))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Character to string conversion failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("spell_Ab!_to_strings", func(t *testing.T) {
|
||||||
|
pwString := "Ab!"
|
||||||
|
spelledString, err := spelling.String(pwString)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("password spelling failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
if spelledString != "Alfa/bravo/EXCLAMATION_POINT" {
|
||||||
|
t.Fatalf(
|
||||||
|
"Spelling pwString 'Ab!' is expected to provide 'Alfa/bravo/EXCLAMATION_POINT', but returned: %q",
|
||||||
|
spelledString)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark: Random number generation
|
||||||
|
func BenchmarkGetRandNum(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = random.GetNum(100000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark: Random char generation
|
||||||
|
func BenchmarkGetRandChar(b *testing.B) {
|
||||||
|
charRange := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\"#/!\\$%&+-*.,?=()[]{}:;~^|"
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = random.GetChar(&charRange, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark: Random char generation
|
||||||
|
func BenchmarkConvertChar(b *testing.B) {
|
||||||
|
|
||||||
|
cfgObj.UseUpperCase = true
|
||||||
|
cfgObj.UseLowerCase = true
|
||||||
|
cfgObj.UseNumber = true
|
||||||
|
cfgObj.UseSpecial = true
|
||||||
|
cfgObj.HumanReadable = false
|
||||||
|
charRange := chars.GetRange(&cfgObj)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
charToConv, _ := random.GetChar(&charRange, 1)
|
||||||
|
charBytes := []byte(charToConv)
|
||||||
|
_, _ = spelling.ConvertCharToName(charBytes[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains function to search a given slice for values
|
||||||
|
func containsByte(allowedBytes []int, currentChar int, t *testing.T) bool {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, charInt := range allowedBytes {
|
||||||
|
if charInt == currentChar {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Loading…
Reference in a new issue