Major refactor so that cmd and lib are separated

This commit is contained in:
Winni Neessen 2021-09-19 17:47:50 +02:00
parent 5ceaf6a777
commit f6cd374412
9 changed files with 270 additions and 603 deletions

View file

@ -1,13 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="AutoImportSettings"> <component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" /> <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="For some reason, the tests on GH fail"> <list default="true" id="fbb0c733-4aa1-4d27-87d5-c7276d8aa613" name="Default Changelist" comment="For some reason, the tests on GH fail">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apg_test.go" beforeDir="false" afterPath="$PROJECT_DIR$/apg_test.go" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apg.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/config.go" beforeDir="false" afterPath="$PROJECT_DIR$/config.go" afterDir="false" /> <change beforePath="$PROJECT_DIR$/apg_test.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/chars.go" beforeDir="false" afterPath="$PROJECT_DIR$/chars/chars.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/convert.go" beforeDir="false" afterPath="$PROJECT_DIR$/spelling/spelling.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/hibp.go" beforeDir="false" afterPath="$PROJECT_DIR$/hibp/hibp.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/rand.go" beforeDir="false" afterPath="$PROJECT_DIR$/random/random.go" afterDir="false" />
</list> </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" />
@ -22,7 +27,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="GOROOT" url="file://$PROJECT_DIR$/../../go1.16.2" /> <component name="GOROOT" url="file://$PROJECT_DIR$/../../go1.17.1" />
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
@ -57,26 +62,30 @@
<property name="WebServerToolWindowFactoryState" value="false" /> <property name="WebServerToolWindowFactoryState" value="false" />
<property name="configurable..is.expanded" value="false" /> <property name="configurable..is.expanded" value="false" />
<property name="configurable.GoLibrariesConfigurable.is.expanded" value="true" /> <property name="configurable.GoLibrariesConfigurable.is.expanded" value="true" />
<property name="go.formatter.settings.were.checked" value="true" />
<property name="go.import.settings.migrated" value="true" /> <property name="go.import.settings.migrated" value="true" />
<property name="go.modules.go.list.on.any.changes.was.set" value="true" />
<property name="go.sdk.automatically.set" value="true" /> <property name="go.sdk.automatically.set" value="true" />
<property name="go.tried.to.enable.integration.vgo.integrator" value="true" /> <property name="go.tried.to.enable.integration.vgo.integrator" value="true" />
<property name="last_opened_file_path" value="$USER_HOME$" /> <property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="settings.editor.selected.configurable" value="inlay.hints.go" /> <property name="settings.editor.selected.configurable" value="inlay.hints.go" />
</component> </component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\Winni Neessen\go\src\apg.go\chars" /> <recent name="$PROJECT_DIR$/chars" />
<recent name="C:\Users\Winni Neessen\go\src\apg.go\lib" /> <recent name="$PROJECT_DIR$/hibp" />
<recent name="C:\Users\Winni Neessen\go\src\apg.go\test" /> <recent name="$PROJECT_DIR$/spelling" />
<recent name="$PROJECT_DIR$/config" />
<recent name="$PROJECT_DIR$/random" />
</key> </key>
</component> </component>
<component name="RunManager" selected="Go Build.Run Application (with newstyle params)"> <component name="RunManager" selected="Go Test.Benchmark Application">
<configuration name="Run Application (show help text)" type="GoApplicationRunConfiguration" factoryName="Go Application"> <configuration name="Run Application (show help text)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" /> <module name="apg.go" />
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-h" /> <parameters value="-h" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -86,7 +95,7 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-v" /> <parameters value="-v" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -96,7 +105,7 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-m 20 -x 20 -C -E abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!]" /> <parameters value="-m 20 -x 20 -C -E abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!]" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -106,7 +115,7 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-m 10 -x 10 -M SLNU -l -n 6" /> <parameters value="-m 10 -x 10 -M SLNU -l -n 6" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -116,7 +125,7 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-m 10 -x 10 -M SLNU -l" /> <parameters value="-m 10 -x 10 -M SLNU -l" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -126,7 +135,7 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-M slNU" /> <parameters value="-M slNU" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -136,7 +145,7 @@
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<parameters value="-m 20 -x 20 -C" /> <parameters value="-m 20 -x 20 -C" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -145,7 +154,17 @@
<module name="apg.go" /> <module name="apg.go" />
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration default="true" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<go_parameters value="-i" />
<kind value="PACKAGE" />
<package value="apg-go" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<method v="2" /> <method v="2" />
@ -154,7 +173,7 @@
<module name="apg.go" /> <module name="apg.go" />
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<framework value="gobench" /> <framework value="gobench" />
@ -164,7 +183,18 @@
<module name="apg.go" /> <module name="apg.go" />
<working_directory value="$PROJECT_DIR$" /> <working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" /> <kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" /> <package value="github.com/wneessen/apg-go/cmd/apg" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<framework value="gotest" />
<method v="2" />
</configuration>
<configuration default="true" type="GoTestRunConfiguration" factoryName="Go Test">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<go_parameters value="-i" />
<kind value="DIRECTORY" />
<package value="apg-go" />
<directory value="$PROJECT_DIR$" /> <directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" /> <filePath value="$PROJECT_DIR$" />
<framework value="gotest" /> <framework value="gotest" />
@ -239,8 +269,8 @@
<integration-enabled>true</integration-enabled> <integration-enabled>true</integration-enabled>
</component> </component>
<component name="com.intellij.coverage.CoverageDataManagerImpl"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/apg_go$Test_Application.out" NAME="Test Application Coverage Results" MODIFIED="1616765230426" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
<SUITE FILE_PATH="coverage/apg_go$TestGetRandNum_in_github_com_wneessen_apg_go__1_.out" NAME="TestGetRandNum in github.com/wneessen/apg.go (1) Coverage Results" MODIFIED="1616594443133" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" /> <SUITE FILE_PATH="coverage/apg_go$TestGetRandNum_in_github_com_wneessen_apg_go__1_.out" NAME="TestGetRandNum in github.com/wneessen/apg.go (1) Coverage Results" MODIFIED="1616594443133" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
<SUITE FILE_PATH="coverage/apg_go$BenchmarkGetRandNum_in_apg_go.out" NAME="BenchmarkGetRandNum in apg.go Coverage Results" MODIFIED="1616342745320" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" /> <SUITE FILE_PATH="coverage/apg_go$BenchmarkGetRandNum_in_apg_go.out" NAME="BenchmarkGetRandNum in apg.go Coverage Results" MODIFIED="1616342745320" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
<SUITE FILE_PATH="coverage/apg_go$Test_Application.out" NAME="Test Application Coverage Results" MODIFIED="1616765230426" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="GoCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
</component> </component>
</project> </project>

113
apg.go
View file

@ -1,113 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
)
// Constants
const DefaultMinLenght int = 12
const DefaultMaxLenght int = 20
const VersionString string = "0.3.2"
type Config struct {
minPassLen int
maxPassLen int
numOfPass int
useComplex bool
useLowerCase bool
useUpperCase bool
useNumber bool
useSpecial bool
humanReadable bool
checkHibp bool
excludeChars string
newStyleModes string
spellPassword bool
ShowHelp bool
showVersion bool
outputMode int
}
// 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 config
log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile)
// Read and parse flags
flag.Usage = func() { _, _ = fmt.Fprintf(os.Stderr, "%s\n", usage) }
var config = parseFlags()
// Show version and exit
if config.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 := getCharRange(&config)
// Generate passwords
for i := 1; i <= config.numOfPass; i++ {
pwLength := getPwLengthFromParams(&config)
pwString, err := getRandChar(&charRange, pwLength)
if err != nil {
log.Fatalf("getRandChar returned an error: %q\n", err)
}
switch config.outputMode {
case 1:
{
spelledPw, err := spellPasswordString(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 config.checkHibp {
isPwned, err := checkHibp(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")
}
}
}
}

View file

@ -1,284 +0,0 @@
package main
import (
"testing"
)
var config Config
// Make sure the flags are initalized
var _ = func() bool {
testing.Init()
config = parseFlags()
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 := getRandNum(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", DefaultMinLenght, DefaultMaxLenght},
{"pwLength 0 to 1", 0, 1},
{"pwLength 1 to 10", 0, 10},
{"pwLength 10 to 100", 10, 100},
}
charRange := getCharRange(&config)
for _, testCase := range testTable {
t.Run(testCase.testName, func(t *testing.T) {
config.minPassLen = testCase.minLength
config.maxPassLen = testCase.maxLength
pwLength := getPwLengthFromParams(&config)
for i := 0; i < 1000; i++ {
pwString, err := getRandChar(&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 := getRandChar(&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 := getRandChar(&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 := getRandChar(&charRange, -2000)
if err == nil {
t.Fatalf("Generated random characters expected to fail, but returned a value => %v",
randChar)
}
})
}
// Test getCharRange() with different config 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) {
config.useLowerCase = testCase.useLowerCase
config.useUpperCase = testCase.useUpperCase
config.useNumber = testCase.useNumber
config.useSpecial = testCase.useSpecial
config.humanReadable = testCase.humanReadable
charRange := getCharRange(&config)
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 := 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) {
config.useUpperCase = true
config.useLowerCase = true
config.useNumber = true
config.useSpecial = true
config.humanReadable = false
charRange := getCharRange(&config)
for _, curChar := range charRange {
_, err := 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 := spellPasswordString(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++ {
_, _ = getRandNum(100000)
}
}
// Benchmark: Random char generation
func BenchmarkGetRandChar(b *testing.B) {
charRange := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\"#/!\\$%&+-*.,?=()[]{}:;~^|"
for i := 0; i < b.N; i++ {
_, _ = getRandChar(&charRange, 20)
}
}
// 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(&config)
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, t *testing.T) bool {
t.Helper()
for _, charInt := range allowedBytes {
if charInt == currentChar {
return true
}
}
return false
}

View file

@ -1,6 +1,7 @@
package main package chars
import ( import (
"github.com/wneessen/apg-go/config"
"regexp" "regexp"
) )
@ -13,13 +14,13 @@ const PwSpecialChars string = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
const PwNumbersHuman string = "23456789" const PwNumbersHuman string = "23456789"
const PwNumbers string = "1234567890" const PwNumbers string = "1234567890"
// Provide the range of available characters based on provided parameters // GetRange provides the range of available characters based on configured parameters
func getCharRange(config *Config) string { func GetRange(config *config.Config) string {
pwUpperChars := PwUpperChars pwUpperChars := PwUpperChars
pwLowerChars := PwLowerChars pwLowerChars := PwLowerChars
pwNumbers := PwNumbers pwNumbers := PwNumbers
pwSpecialChars := PwSpecialChars pwSpecialChars := PwSpecialChars
if config.humanReadable { if config.HumanReadable {
pwUpperChars = PwUpperCharsHuman pwUpperChars = PwUpperCharsHuman
pwLowerChars = PwLowerCharsHuman pwLowerChars = PwLowerCharsHuman
pwNumbers = PwNumbersHuman pwNumbers = PwNumbersHuman
@ -27,20 +28,20 @@ func getCharRange(config *Config) string {
} }
var charRange string var charRange string
if config.useLowerCase { if config.UseLowerCase {
charRange = charRange + pwLowerChars charRange = charRange + pwLowerChars
} }
if config.useUpperCase { if config.UseUpperCase {
charRange = charRange + pwUpperChars charRange = charRange + pwUpperChars
} }
if config.useNumber { if config.UseNumber {
charRange = charRange + pwNumbers charRange = charRange + pwNumbers
} }
if config.useSpecial { if config.UseSpecial {
charRange = charRange + pwSpecialChars charRange = charRange + pwSpecialChars
} }
if config.excludeChars != "" { if config.ExcludeChars != "" {
regExp := regexp.MustCompile("[" + regexp.QuoteMeta(config.excludeChars) + "]") regExp := regexp.MustCompile("[" + regexp.QuoteMeta(config.ExcludeChars) + "]")
charRange = regExp.ReplaceAllLiteralString(charRange, "") charRange = regExp.ReplaceAllLiteralString(charRange, "")
} }

164
config.go
View file

@ -1,164 +0,0 @@
package main
import (
"flag"
"log"
)
// Parse the CLI flags
func parseFlags() Config {
var switchConf Config
defaultSwitches := Config{
useLowerCase: true,
useUpperCase: true,
useNumber: true,
useSpecial: false,
useComplex: false,
humanReadable: false,
}
config := Config{
useLowerCase: defaultSwitches.useLowerCase,
useUpperCase: defaultSwitches.useUpperCase,
useNumber: defaultSwitches.useNumber,
useSpecial: defaultSwitches.useSpecial,
useComplex: defaultSwitches.useComplex,
humanReadable: defaultSwitches.humanReadable,
}
// Read and set all flags
flag.BoolVar(&switchConf.useLowerCase, "L", false, "Use lower case characters in passwords")
flag.BoolVar(&switchConf.useUpperCase, "U", false, "Use upper case characters in passwords")
flag.BoolVar(&switchConf.useNumber, "N", false, "Use numerich characters in passwords")
flag.BoolVar(&switchConf.useSpecial, "S", false, "Use special characters in passwords")
flag.BoolVar(&switchConf.useComplex, "C", false, "Generate complex passwords (implies -L -U -N -S, disables -H)")
flag.BoolVar(&switchConf.humanReadable, "H", false, "Generate human-readable passwords")
flag.BoolVar(&config.spellPassword, "l", false, "Spell generated password")
flag.BoolVar(&config.checkHibp, "p", false, "Check the HIBP database if the generated password was leaked before")
flag.BoolVar(&config.showVersion, "v", false, "Show version")
flag.IntVar(&config.minPassLen, "m", DefaultMinLenght, "Minimum password length")
flag.IntVar(&config.maxPassLen, "x", DefaultMaxLenght, "Maxiumum password length")
flag.IntVar(&config.numOfPass, "n", 6, "Number of passwords to generate")
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()
// Invert-switch the defaults
if switchConf.useLowerCase {
config.useLowerCase = !defaultSwitches.useLowerCase
}
if switchConf.useUpperCase {
config.useUpperCase = !defaultSwitches.useUpperCase
}
if switchConf.useNumber {
config.useNumber = !defaultSwitches.useNumber
}
if switchConf.useSpecial {
config.useSpecial = !defaultSwitches.useSpecial
}
if switchConf.useComplex {
config.useComplex = !defaultSwitches.useComplex
}
if switchConf.humanReadable {
config.humanReadable = !defaultSwitches.humanReadable
}
// Parse additional parameters and new-style switches
parseParams(&config)
return config
}
// Parse the parameters and set the according config flags
func parseParams(config *Config) {
parseNewStyleParams(config)
// 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 {
log.Fatalf("No password mode set. Cannot generate password from empty character set.")
}
// Set output mode
if config.spellPassword {
config.outputMode = 1
}
}
// Get the password length from the given cli flags
func getPwLengthFromParams(config *Config) int {
if config.minPassLen > config.maxPassLen {
config.maxPassLen = config.minPassLen
}
lenDiff := config.maxPassLen - config.minPassLen + 1
randAdd, err := getRandNum(lenDiff)
if err != nil {
log.Fatalf("Failed to generated password length: %v", err)
}
retVal := config.minPassLen + randAdd
if retVal <= 0 {
return 1
}
return retVal
}
// Parse the new style parameters
func parseNewStyleParams(config *Config) {
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:
log.Fatalf("Unknown password style parameter: %q\n", string(curParam))
}
}
}

193
config/config.go Normal file
View file

@ -0,0 +1,193 @@
package config
import (
"flag"
"github.com/wneessen/apg-go/random"
"log"
)
// Config is a struct that holds the different config parameters for the apg-go
// application
type Config struct {
MinPassLen int
MaxPassLen int
NumOfPass int
UseComplex bool
UseLowerCase bool
UseUpperCase bool
UseNumber bool
UseSpecial bool
HumanReadable bool
CheckHibp bool
ExcludeChars string
NewStyleModes string
SpellPassword bool
ShowHelp bool
ShowVersion bool
OutputMode int
}
// DefaultMinLength reflects the default minimum length of a generated password
const DefaultMinLength int = 12
// DefaultMaxLength reflects the default maximum length of a generated password
const DefaultMaxLength int = 20
// New parses the CLI flags and returns a new config object
func New() Config {
var switchConf Config
defaultSwitches := Config{
UseLowerCase: true,
UseUpperCase: true,
UseNumber: true,
UseSpecial: false,
UseComplex: false,
HumanReadable: false,
}
config := Config{
UseLowerCase: defaultSwitches.UseLowerCase,
UseUpperCase: defaultSwitches.UseUpperCase,
UseNumber: defaultSwitches.UseNumber,
UseSpecial: defaultSwitches.UseSpecial,
UseComplex: defaultSwitches.UseComplex,
HumanReadable: defaultSwitches.HumanReadable,
}
// Read and set all flags
flag.BoolVar(&switchConf.UseLowerCase, "L", false, "Use lower case characters in passwords")
flag.BoolVar(&switchConf.UseUpperCase, "U", false, "Use upper case characters in passwords")
flag.BoolVar(&switchConf.UseNumber, "N", false, "Use numerich characters in passwords")
flag.BoolVar(&switchConf.UseSpecial, "S", false, "Use special characters in passwords")
flag.BoolVar(&switchConf.UseComplex, "C", false, "Generate complex passwords (implies -L -U -N -S, disables -H)")
flag.BoolVar(&switchConf.HumanReadable, "H", false, "Generate human-readable passwords")
flag.BoolVar(&config.SpellPassword, "l", false, "Spell generated password")
flag.BoolVar(&config.CheckHibp, "p", false, "Check the HIBP database if the generated password was leaked before")
flag.BoolVar(&config.ShowVersion, "v", false, "Show version")
flag.IntVar(&config.MinPassLen, "m", DefaultMinLength, "Minimum password length")
flag.IntVar(&config.MaxPassLen, "x", DefaultMaxLength, "Maxiumum password length")
flag.IntVar(&config.NumOfPass, "n", 6, "Number of passwords to generate")
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()
// Invert-switch the defaults
if switchConf.UseLowerCase {
config.UseLowerCase = !defaultSwitches.UseLowerCase
}
if switchConf.UseUpperCase {
config.UseUpperCase = !defaultSwitches.UseUpperCase
}
if switchConf.UseNumber {
config.UseNumber = !defaultSwitches.UseNumber
}
if switchConf.UseSpecial {
config.UseSpecial = !defaultSwitches.UseSpecial
}
if switchConf.UseComplex {
config.UseComplex = !defaultSwitches.UseComplex
}
if switchConf.HumanReadable {
config.HumanReadable = !defaultSwitches.HumanReadable
}
// Parse additional parameters and new-style switches
parseParams(&config)
return config
}
// Parse the parameters and set the according config flags
func parseParams(config *Config) {
parseNewStyleParams(config)
// 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 {
log.Fatalf("No password mode set. Cannot generate password from empty character set.")
}
// Set output mode
if config.SpellPassword {
config.OutputMode = 1
}
}
// Parse the new style parameters
func parseNewStyleParams(config *Config) {
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:
log.Fatalf("Unknown password style parameter: %q\n", string(curParam))
}
}
}
// GetPwLengthFromParams extracts the password length from the given cli flags and stores
// in the provided config object
func GetPwLengthFromParams(config *Config) int {
if config.MinPassLen > config.MaxPassLen {
config.MaxPassLen = config.MinPassLen
}
lenDiff := config.MaxPassLen - config.MinPassLen + 1
randAdd, err := random.GetNum(lenDiff)
if err != nil {
log.Fatalf("Failed to generated password length: %v", err)
}
retVal := config.MinPassLen + randAdd
if retVal <= 0 {
return 1
}
return retVal
}

View file

@ -1,4 +1,4 @@
package main package hibp
import ( import (
"bufio" "bufio"
@ -10,7 +10,8 @@ import (
"time" "time"
) )
func checkHibp(p string) (bool, error) { // Check queries the HIBP database and checks if a given string is was found
func Check(p string) (bool, error) {
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(p))) shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(p)))
firstPart := shaSum[0:5] firstPart := shaSum[0:5]
secondPart := shaSum[5:] secondPart := shaSum[5:]

View file

@ -1,4 +1,4 @@
package main package random
import ( import (
"crypto/rand" "crypto/rand"
@ -6,9 +6,9 @@ import (
"math/big" "math/big"
) )
// Generate random characters based on given character range // GetChar generates random characters based on given character range
// and password length // and password length
func getRandChar(charRange *string, pwLength int) (string, error) { func GetChar(charRange *string, pwLength int) (string, error) {
if pwLength <= 0 { if pwLength <= 0 {
err := fmt.Errorf("provided pwLength value is <= 0: %v", pwLength) err := fmt.Errorf("provided pwLength value is <= 0: %v", pwLength)
return "", err return "", err
@ -17,7 +17,7 @@ func getRandChar(charRange *string, pwLength int) (string, error) {
charSlice := []byte(*charRange) charSlice := []byte(*charRange)
returnString := make([]byte, pwLength) returnString := make([]byte, pwLength)
for i := 0; i < pwLength; i++ { for i := 0; i < pwLength; i++ {
randNum, err := getRandNum(availCharsLength) randNum, err := GetNum(availCharsLength)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -26,8 +26,8 @@ func getRandChar(charRange *string, pwLength int) (string, error) {
return string(returnString), nil return string(returnString), nil
} }
// Generate a random number with given maximum value // GetNum generates a random number with given maximum value
func getRandNum(maxNum int) (int, error) { func GetNum(maxNum int) (int, error) {
if maxNum <= 0 { if maxNum <= 0 {
err := fmt.Errorf("provided maxNum is <= 0: %v", maxNum) err := fmt.Errorf("provided maxNum is <= 0: %v", maxNum)
return 0, err return 0, err

View file

@ -1,4 +1,4 @@
package main package spelling
import ( import (
"fmt" "fmt"
@ -80,10 +80,11 @@ var (
} }
) )
func spellPasswordString(pwString string) (string, error) { // String returns an english spelled version of the given string
func String(pwString string) (string, error) {
var returnString []string var returnString []string
for _, curChar := range pwString { for _, curChar := range pwString {
curSpellString, err := convertCharToName(byte(curChar)) curSpellString, err := ConvertCharToName(byte(curChar))
if err != nil { if err != nil {
return "", err return "", err
} }
@ -92,7 +93,9 @@ func spellPasswordString(pwString string) (string, error) {
return strings.Join(returnString, "/"), nil return strings.Join(returnString, "/"), nil
} }
func convertCharToName(charByte byte) (string, error) { // ConvertCharToName converts a given ascii byte into the corresponding english spelled
// name
func ConvertCharToName(charByte byte) (string, error) {
var returnString string var returnString string
if charByte > 64 && charByte < 91 { if charByte > 64 && charByte < 91 {
returnString = alphabetNames[charByte] returnString = alphabetNames[charByte]