Merge pull request #31 from wneessen/cmd_refactor

Separate CLI app from library functions
This commit is contained in:
Winni Neessen 2021-09-23 09:45:03 +02:00 committed by GitHub
commit 81f15c4ef2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 313 additions and 514 deletions

23
.cirrus.yml Normal file
View file

@ -0,0 +1,23 @@
container:
image: golang:latest
env:
GOPROXY: https://proxy.golang.org
lint_task:
name: GolangCI Lint
container:
image: golangci/golangci-lint:latest
run_script: golangci-lint run -v --timeout 5m0s --out-format json > lint-report.json
always:
golangci_artifacts:
path: lint-report.json
type: text/json
format: golangci
build_task:
modules_cache:
folder: $GOPATH/pkg/mod
get_script: go get github.com/wneessen/apg-go/cmd/apg
build_script: go build github.com/wneessen/apg-go/cmd/apg
test_script: go test github.com/wneessen/apg-go/cmd/apg

View file

@ -17,10 +17,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
go-version: 1.17
- name: Build
run: go build -o apg -v .
run: go build -o apg -v github.com/wneessen/apg-go/cmd/apg
- name: Test
run: go test -v .
run: go test -v github.com/wneessen/apg-go/cmd/apg

8
.idea/.gitignore vendored
View file

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View file

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/apg.go.iml" filepath="$PROJECT_DIR$/.idea/apg.go.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/apg-go.iml" filepath="$PROJECT_DIR$/.idea/apg-go.iml" />
</modules>
</component>
</project>

View file

@ -1,187 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
<option name="autoReloadType" value="ALL" />
</component>
<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="e32960c0-29e5-4669-9fc2-ef12314486ce" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/apg-go.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/apg-go.iml" 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$/config.go" beforeDir="false" afterPath="$PROJECT_DIR$/config.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/config.go" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.go" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Go File" />
<option value="Go Application" />
</list>
</option>
</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">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="GoLibraries">
<option name="indexEntireGoPath" value="false" />
</component>
<component name="ProjectId" id="1pxyItB9dnh8hLHpqlNK6NktVaL" />
<component name="ProjectLevelVcsManager">
<OptionsSetting value="false" id="Update" />
</component>
<component name="ProjectId" id="1yX0zXHdpGPxqctARqJlfIL2eFe" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="DefaultGoTemplateProperty" value="Go Application" />
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="WebServerToolWindowFactoryState" value="true" />
<property name="configurable..is.expanded" value="false" />
<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.modules.go.list.on.any.changes.was.set" value="true" />
<property name="go.sdk.automatically.set" value="true" />
<property name="go.tried.to.enable.integration.vgo.integrator" value="true" />
<property name="last_opened_file_path" value="$USER_HOME$" />
<property name="settings.editor.selected.configurable" value="inlay.hints.go" />
</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\Winni Neessen\go\src\apg.go\chars" />
<recent name="C:\Users\Winni Neessen\go\src\apg.go\lib" />
<recent name="C:\Users\Winni Neessen\go\src\apg.go\test" />
</key>
</component>
<component name="RunManager" selected="Go Build.Run Application (with newstyle params)">
<configuration name="Run Application (show help text)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-h" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application (show version string)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-v" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application (with excludes)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-m 20 -x 20 -C -E abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!]" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application (with newstyle params and spelling and multiple pws)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-m 10 -x 10 -M SLNU -l -n 6" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application (with newstyle params and spelling)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-m 10 -x 10 -M SLNU -l" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application (with newstyle params)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-M slNU" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application (with params)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-m 20 -x 20 -C" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Run Application" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Benchmark Application" type="GoTestRunConfiguration" factoryName="Go Test">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<framework value="gobench" />
<method v="2" />
</configuration>
<configuration name="Test Application" type="GoTestRunConfiguration" factoryName="Go Test">
<module name="apg.go" />
<working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" />
<package value="github.com/wneessen/apg-go" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<framework value="gotest" />
<method v="2" />
</configuration>
<list>
<item itemvalue="Go Build.Run Application" />
<item itemvalue="Go Build.Run Application (with params)" />
<item itemvalue="Go Build.Run Application (with excludes)" />
<item itemvalue="Go Build.Run Application (with newstyle params)" />
<item itemvalue="Go Build.Run Application (with newstyle params and spelling)" />
<item itemvalue="Go Build.Run Application (with newstyle params and spelling and multiple pws)" />
<item itemvalue="Go Build.Run Application (show help text)" />
<item itemvalue="Go Build.Run Application (show version string)" />
<item itemvalue="Go Test.Test Application" />
<item itemvalue="Go Test.Benchmark Application" />
</list>
<property name="settings.editor.selected.configurable" value="go.vgo" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TypeScriptGeneratedFilesManager">
@ -192,55 +48,13 @@
<map>
<entry key="MAIN">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="main" />
</list>
</value>
</entry>
</map>
</option>
</State>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Updated README and gitignore files" />
<MESSAGE value="v0.2.1: Support for more platforms added" />
<MESSAGE value="v0.2.3: More chars, cleanup and better test coverage&#10;- Added more special characters&#10;- Fixed issue with ` character&#10;- Added lots of tests&#10;- Moved character range generation into it's own function&#10;- Better error handling&#10;- A bit of code cleanup" />
<MESSAGE value="Split up functions into seperate files" />
<MESSAGE value="Added spelling of pws. Ready for v0.2.4" />
<MESSAGE value="Fixes in getCharRange() tests" />
<MESSAGE value="excludeChars are now escaped before using them as regExp" />
<MESSAGE value="Added sane defaults" />
<MESSAGE value="Updates README.md. We are ready for v0.2.5" />
<MESSAGE value="Break the switch/case" />
<MESSAGE value="v0.2.6 - converted to go module structure&#10;- Better logging&#10;- Better error handling&#10;- Removed Makefile since github takes care of building/releasing" />
<MESSAGE value="Tests cleanup. Used structs for tests instead of repeating code" />
<MESSAGE value="v0.2.7: Switched global config to it's own package" />
<MESSAGE value="Updated .gitignore" />
<MESSAGE value="Removed .idea from .gitignore" />
<MESSAGE value="Let's have centralized IDE config" />
<MESSAGE value="New DEV branch" />
<MESSAGE value="v0.2.9: Replaced standard go-help with custom usage text" />
<MESSAGE value="v0.3.0: Unified the naming convention" />
<MESSAGE value="v0.3.1: New password length behaviour&#10;&#10;To address issue #13, the password length behaviour of the &#10;original APG has been reproduced. Previously, when a minLength&#10;of 5 and a maxLength of 10 was given, apg-go se the pwLength to&#10;the preferred maxLength.&#10;&#10;With v0.3.1 it will choose a random length between minLength and&#10;maxLength instead, same as the original C-lang apg did. For this&#10;the minLength has been defaulted to a sane value of 12 (instead &#10;of the 8 of the original apg). The default for maxLength stayed&#10;at 20.&#10;&#10;Also the default number of generated passwords has been changed &#10;from 1 to 6, to replicate the behaviour of the original apg." />
<MESSAGE value="For some reason, the tests on GH fail" />
<option name="LAST_COMMIT_MESSAGE" value="For some reason, the tests on GH fail" />
</component>
<component name="VgoProject">
<integration-enabled>true</integration-enabled>
</component>
<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$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" />
</component>
</project>

View file

@ -18,7 +18,7 @@ prepare() {
build() {
cd "${pkgname}-${pkgver}"
go build -ldflags="-s -w" -o build/${pkgname} ./...
go build -ldflags="-s -w" -o build/${pkgname} github.com/wneessen/apg-go/cmd/apg
}
package() {

View file

@ -16,6 +16,6 @@ PERMIT_PACKAGE = Yes
MODULES = lang/go
MODGO_TYPE = bin
ALL_TARGET = wneessen/apg-go
ALL_TARGET = wneessen/apg-go/cmd/apg
.include <bsd.port.mk>

View file

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

View file

@ -3,33 +3,17 @@ 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"
"github.com/wneessen/go-hibp"
"log"
"os"
"time"
)
// 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
}
const VersionString string = "0.4.0-dev"
// Help text
const usage = `apg-go // A "Automated Password Generator"-clone
@ -58,37 +42,37 @@ Options:
// Main function that generated the passwords and returns them
func main() {
// Log config
// 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 config = parseFlags()
var cfgObj = config.New()
// Show version and exit
if config.showVersion {
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 := getCharRange(&config)
charRange := chars.GetRange(&cfgObj)
// Generate passwords
for i := 1; i <= config.numOfPass; i++ {
pwLength := getPwLengthFromParams(&config)
pwString, err := getRandChar(&charRange, pwLength)
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)
log.Fatalf("error generating random character range: %s\n", err)
}
switch config.outputMode {
switch cfgObj.OutputMode {
case 1:
{
spelledPw, err := spellPasswordString(pwString)
spelledPw, err := spelling.String(pwString)
if err != nil {
log.Fatalf("spellPasswordString returned an error: %q\n", err.Error())
log.Fatalf("error spelling out password: %s\n", err)
}
fmt.Printf("%v (%v)\n", pwString, spelledPw)
break
@ -100,12 +84,13 @@ func main() {
}
}
if config.checkHibp {
isPwned, err := checkHibp(pwString)
if cfgObj.CheckHibp {
hc := hibp.New(hibp.WithHttpTimeout(time.Second*2), hibp.WithPwnedPadding())
pwnObj, _, err := hc.PwnedPassApi.CheckPassword(pwString)
if err != nil {
log.Printf("unable to check HIBP database: %v", err)
}
if isPwned {
if pwnObj != nil && pwnObj.Count != 0 {
fmt.Print("^-- !!WARNING: The previously generated password was found in HIPB database. Do not use it!!\n")
}
}

View file

@ -1,15 +1,19 @@
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 config Config
var cfgObj config.Config
// Make sure the flags are initalized
var _ = func() bool {
testing.Init()
config = parseFlags()
cfgObj = config.New()
return true
}()
@ -30,7 +34,7 @@ func TestGetRandNum(t *testing.T) {
for _, testCase := range testTable {
t.Run(testCase.testName, func(t *testing.T) {
randNum, err := getRandNum(testCase.givenVal)
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",
@ -60,20 +64,20 @@ func TestGenLength(t *testing.T) {
minLength int
maxLength int
}{
{"pwLength defaults", DefaultMinLenght, DefaultMaxLenght},
{"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 := getCharRange(&config)
charRange := chars.GetRange(&cfgObj)
for _, testCase := range testTable {
t.Run(testCase.testName, func(t *testing.T) {
config.minPassLen = testCase.minLength
config.maxPassLen = testCase.maxLength
pwLength := getPwLengthFromParams(&config)
cfgObj.MinPassLen = testCase.minLength
cfgObj.MaxPassLen = testCase.maxLength
pwLength := config.GetPwLengthFromParams(&cfgObj)
for i := 0; i < 1000; i++ {
pwString, err := getRandChar(&charRange, pwLength)
pwString, err := random.GetChar(&charRange, pwLength)
if err != nil {
t.Errorf("getRandChar returned an error: %q", err)
}
@ -95,7 +99,7 @@ func TestGenLength(t *testing.T) {
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)
randChar, err := random.GetChar(&charRange, 1)
if err != nil {
t.Fatalf("Random character generation failed => %v", err.Error())
}
@ -106,7 +110,7 @@ func TestGetRandChar(t *testing.T) {
t.Run("return_value_has_specific_length", func(t *testing.T) {
charRange := "ABC"
randChar, err := getRandChar(&charRange, 1000)
randChar, err := random.GetChar(&charRange, 1000)
if err != nil {
t.Fatalf("Random character generation failed => %v", err.Error())
}
@ -118,7 +122,7 @@ func TestGetRandChar(t *testing.T) {
t.Run("fail", func(t *testing.T) {
charRange := "ABC"
randChar, err := getRandChar(&charRange, -2000)
randChar, err := random.GetChar(&charRange, -2000)
if err == nil {
t.Fatalf("Generated random characters expected to fail, but returned a value => %v",
randChar)
@ -126,7 +130,7 @@ func TestGetRandChar(t *testing.T) {
})
}
// Test getCharRange() with different config settings
// 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'}
@ -162,12 +166,12 @@ func TestGetCharRange(t *testing.T) {
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)
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 {
@ -194,7 +198,7 @@ func TestConvert(t *testing.T) {
for _, testCase := range testTable {
t.Run(testCase.testName, func(t *testing.T) {
charToString, err := convertCharToName(testCase.givenVal)
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",
@ -213,14 +217,14 @@ func TestConvert(t *testing.T) {
}
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)
cfgObj.UseUpperCase = true
cfgObj.UseLowerCase = true
cfgObj.UseNumber = true
cfgObj.UseSpecial = true
cfgObj.HumanReadable = false
charRange := chars.GetRange(&cfgObj)
for _, curChar := range charRange {
_, err := convertCharToName(byte(curChar))
_, err := spelling.ConvertCharToName(byte(curChar))
if err != nil {
t.Fatalf("Character to string conversion failed: %v", err.Error())
}
@ -228,7 +232,7 @@ func TestConvert(t *testing.T) {
})
t.Run("spell_Ab!_to_strings", func(t *testing.T) {
pwString := "Ab!"
spelledString, err := spellPasswordString(pwString)
spelledString, err := spelling.String(pwString)
if err != nil {
t.Fatalf("password spelling failed: %v", err.Error())
}
@ -243,7 +247,7 @@ func TestConvert(t *testing.T) {
// Benchmark: Random number generation
func BenchmarkGetRandNum(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = getRandNum(100000)
_, _ = random.GetNum(100000)
}
}
@ -251,23 +255,23 @@ func BenchmarkGetRandNum(b *testing.B) {
func BenchmarkGetRandChar(b *testing.B) {
charRange := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\"#/!\\$%&+-*.,?=()[]{}:;~^|"
for i := 0; i < b.N; i++ {
_, _ = getRandChar(&charRange, 20)
_, _ = random.GetChar(&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)
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, _ := getRandChar(&charRange, 1)
charToConv, _ := random.GetChar(&charRange, 1)
charBytes := []byte(charToConv)
_, _ = convertCharToName(charBytes[0])
_, _ = spelling.ConvertCharToName(charBytes[0])
}
}

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

181
config/config.go Normal file
View file

@ -0,0 +1,181 @@
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 &&
!config.UseLowerCase &&
!config.UseNumber &&
!config.UseSpecial {
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
case 's':
config.UseSpecial = false
case 'N':
config.UseNumber = true
case 'n':
config.UseNumber = false
case 'L':
config.UseLowerCase = true
case 'l':
config.UseLowerCase = false
case 'U':
config.UseUpperCase = true
case 'u':
config.UseUpperCase = false
case 'H':
config.HumanReadable = true
case 'h':
config.HumanReadable = false
case 'C':
config.UseComplex = true
case 'c':
config.UseComplex = false
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
}

2
go.mod
View file

@ -1,3 +1,5 @@
module github.com/wneessen/apg-go
go 1.16
require github.com/wneessen/go-hibp v1.0.0

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/wneessen/go-hibp v1.0.0 h1:XmjymVwdhH06TVzU/ahlYzwAhX3OgxoxvyJ3HPa8gks=
github.com/wneessen/go-hibp v1.0.0/go.mod h1:Ldg6DQg4fMCveVKgL+RL9Jy+9TsljjAP704Ix8X3jOw=

44
hibp.go
View file

@ -1,44 +0,0 @@
package main
import (
"bufio"
"crypto/sha1"
"fmt"
"log"
"net/http"
"strings"
"time"
)
func checkHibp(p string) (bool, error) {
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(p)))
firstPart := shaSum[0:5]
secondPart := shaSum[5:]
isPwned := false
httpClient := &http.Client{Timeout: time.Second * 2}
httpRes, err := httpClient.Get("https://api.pwnedpasswords.com/range/" + firstPart)
if err != nil {
return false, err
}
defer func() {
err := httpRes.Body.Close()
if err != nil {
log.Printf("error while closing HTTP response body: %v\n", err)
}
}()
scanObj := bufio.NewScanner(httpRes.Body)
for scanObj.Scan() {
scanLine := strings.SplitN(scanObj.Text(), ":", 2)
if strings.ToLower(scanLine[0]) == secondPart {
isPwned = true
break
}
}
if err := scanObj.Err(); err != nil {
return isPwned, err
}
return isPwned, nil
}

View file

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

View file

@ -1,4 +1,4 @@
package main
package spelling
import (
"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
for _, curChar := range pwString {
curSpellString, err := convertCharToName(byte(curChar))
curSpellString, err := ConvertCharToName(byte(curChar))
if err != nil {
return "", err
}
@ -92,7 +93,9 @@ func spellPasswordString(pwString string) (string, error) {
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
if charByte > 64 && charByte < 91 {
returnString = alphabetNames[charByte]