Refactor generator and add config options

Refactored the generator to include a new config option, changed function signatures to follow the new structure, and renamed the function 'RandomString' to 'RandomStringFromCharRange' for clarity. Also, added a new mode and algorithm feature to enhance password generation. Furthermore, added several tests for new features and configurations. Adapted the CLI to use the new configuration approach. This refactoring was necessary to improve the customizability and clarity of the password generation process. Fixed minor issues and added '.gitignore' for clean commits in the future.
This commit is contained in:
Winni Neessen 2023-08-04 16:02:58 +02:00
parent e94b1ade5c
commit ac97b94ec9
Signed by: wneessen
GPG key ID: 5F3AF39B820C119D
10 changed files with 366 additions and 46 deletions

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Local testfiles and auth data
.auth
examples/*
# SonarQube
.scannerwork/
# IDEA specific ignores
.idea/

29
algo.go Normal file
View file

@ -0,0 +1,29 @@
package apg
// Algorithm is a type wrapper for an int type to represent different
// password generation algorithm
type Algorithm int
const (
// Pronouncable represents the algorithm for pronouncable passwords
// (koremutake syllables)
Pronouncable Algorithm = iota
// Random represents the algorithm for purely random passwords according
// to the provided password modes/flags
Random
// Unsupported represents an unsupported algorithm
Unsupported
)
// IntToAlgo takes an int value as input and returns the corresponding
// Algorithm
func IntToAlgo(a int) Algorithm {
switch a {
case 0:
return Pronouncable
case 1:
return Random
default:
return Unsupported
}
}

11
apg.go
View file

@ -1,12 +1,19 @@
package apg
// VERSION represents the version string
const VERSION = "2.0.0"
// Generator is the password generator type of the APG package
type Generator struct {
// charRange is the range of character used for the
charRange string
// config is a pointer to the apg config instance
config *Config
}
// New returns a new password Generator type
func New() *Generator {
return &Generator{}
func New(c *Config) *Generator {
return &Generator{
config: c,
}
}

View file

@ -3,18 +3,65 @@
package main
import (
"fmt"
"flag"
"os"
"github.com/wneessen/apg-go"
)
func main() {
g := apg.New()
rb, err := g.RandomBytes(8)
if err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
fmt.Printf("Random: %#v\n", rb)
c := apg.NewConfig()
// Configure and parse the CLI flags
flag.Int64Var(&c.MinLength, "m", c.MinLength, "")
flag.Int64Var(&c.MaxLength, "x", c.MaxLength, "")
flag.Int64Var(&c.NumberPass, "n", c.NumberPass, "")
flag.Usage = usage
flag.Parse()
/*
g := apg.New(c)
rb, err := g.RandomBytes(c.MinLength)
if err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
fmt.Printf("Random: %#v\n", rb)
*/
}
// usage is used by the flag package to display the CLI usage message
func usage() {
// Usage text
const ut = `apg-go // A "Automated Password Generator"-clone
Copyleft (c) 2021-2023 Winni Neessen
apg [-a <algo>] [-m <length>] [-x <length>] [-L] [-U] [-N] [-S] [-H] [-C]
[-l] [-M mode] [-E char_string] [-n num_of_pass] [-v] [-h] [-t]
Options:
-a ALGORITH Choose the password generation algorithm (Default: 1)
- 0: pronounceable password generation (koremutake syllables)
- 1: random password generation according to password modes/flags
-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)
- Note: this feature requires internet connectivity
-h Show this help text
-v Show version string
`
_, _ = os.Stderr.WriteString(ut)
}

67
config.go Normal file
View file

@ -0,0 +1,67 @@
package apg
// List of default values for Config instances
const (
// DefaultMinLength reflects the default minimum length of a generated password
DefaultMinLength int64 = 12
// DefaultMaxLength reflects the default maximum length of a generated password
DefaultMaxLength int64 = 20
// DefaultNumberPass reflects the default amount of passwords returned by the generator
DefaultNumberPass int64 = 6
)
// Config represents the apg.Generator config parameters
type Config struct {
// Algo
Algorithm Algorithm
// MaxLength sets the maximum length for a generated password
MaxLength int64
// MinLength sets the minimum length for a generated password
MinLength int64
// NumberPass sets the number of passwords that are generated
// and returned by the generator
NumberPass int64
}
// Option is a function that can override default Config settings
type Option func(*Config)
// NewConfig creates a new Config instance and pre-fills it with sane
// default settings. The Config is returned as pointer value
func NewConfig(o ...Option) *Config {
c := &Config{
MaxLength: DefaultMaxLength,
MinLength: DefaultMinLength,
NumberPass: DefaultNumberPass,
}
// Override defaults with optionally provided config.Option functions
for _, co := range o {
if co == nil {
continue
}
co(c)
}
return c
}
// WithMinLength overrides the minimum password length
func WithMinLength(l int64) Option {
return func(c *Config) {
c.MinLength = l
}
}
// WithMaxLength overrides the maximum password length
func WithMaxLength(l int64) Option {
return func(c *Config) {
c.MaxLength = l
}
}
// WithNumberPass overrides the number of generated passwords setting
func WithNumberPass(n int64) Option {
return func(c *Config) {
c.NumberPass = n
}
}

69
config_test.go Normal file
View file

@ -0,0 +1,69 @@
package apg
import (
"testing"
)
func TestNewConfig(t *testing.T) {
c := NewConfig()
if c == nil {
t.Errorf("NewConfig() failed, expected config pointer but got nil")
return
}
c = NewConfig(nil)
if c == nil {
t.Errorf("NewConfig() failed, expected config pointer but got nil")
return
}
if c.MinLength != DefaultMinLength {
t.Errorf("NewConfig() failed, expected min length: %d, got: %d", DefaultMinLength,
c.MinLength)
}
if c.MaxLength != DefaultMaxLength {
t.Errorf("NewConfig() failed, expected max length: %d, got: %d", DefaultMaxLength,
c.MaxLength)
}
if c.NumberPass != DefaultNumberPass {
t.Errorf("NewConfig() failed, expected number of passwords: %d, got: %d",
DefaultNumberPass, c.NumberPass)
}
}
func TestWithMaxLength(t *testing.T) {
var e int64 = 123
c := NewConfig(WithMaxLength(e))
if c == nil {
t.Errorf("NewConfig(WithMaxLength()) failed, expected config pointer but got nil")
return
}
if c.MaxLength != e {
t.Errorf("NewConfig(WithMaxLength()) failed, expected max length: %d, got: %d",
e, c.MaxLength)
}
}
func TestWithMinLength(t *testing.T) {
var e int64 = 1
c := NewConfig(WithMinLength(e))
if c == nil {
t.Errorf("NewConfig(WithMinLength()) failed, expected config pointer but got nil")
return
}
if c.MinLength != e {
t.Errorf("NewConfig(WithMinLength()) failed, expected min length: %d, got: %d",
e, c.MinLength)
}
}
func TestWithNumberPass(t *testing.T) {
var e int64 = 123
c := NewConfig(WithNumberPass(e))
if c == nil {
t.Errorf("NewConfig(WithNumberPass()) failed, expected config pointer but got nil")
return
}
if c.NumberPass != e {
t.Errorf("NewConfig(WithNumberPass()) failed, expected number of passwords: %d, got: %d",
e, c.NumberPass)
}
}

51
mode.go Normal file
View file

@ -0,0 +1,51 @@
package apg
// Mode represents a mode of characters
type Mode uint8
const (
// ModeNumber sets the bitmask to include numbers in the generated passwords
ModeNumber = 1 << iota
// ModeLowerCase sets the bitmask to include lower case characters in the
// generated passwords
ModeLowerCase
// ModeUpperCase sets the bitmask to include upper case characters in the
// generated passwords
ModeUpperCase
// ModeSpecial sets the bitmask to include special characters in the
// generated passwords
ModeSpecial
// ModeHumanReadable sets the bitmask to generate human readable passwords
ModeHumanReadable
)
const (
// CharRangeAlphaLower represents all lower-case alphabetical characters
CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz"
// CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters
CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz"
// CharRangeAlphaUpper represents all upper-case alphabetical characters
CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters
CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ"
// CharRangeNumber represents all numerical characters
CharRangeNumber = "1234567890"
// CharRangeNumberHuman represents all human-readable numerical characters
CharRangeNumberHuman = "23456789"
// CharRangeSpecial represents all special characters
CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~`
// CharRangeSpecialHuman represents all human-readable special characters
CharRangeSpecialHuman = `#%*+-:;=`
)
// SetMode sets a specific Mode to a given Mode bitmask
func SetMode(b, m Mode) Mode { return b | m }
// ClearMode clears a specific Mode from a given Mode bitmask
func ClearMode(b, m Mode) Mode { return b &^ m }
// ToggleMode toggles a specific Mode in a given Mode bitmask
func ToggleMode(b, m Mode) Mode { return b ^ m }
// HasMode returns true if a given Mode bitmask holds a specific Mode
func HasMode(b, m Mode) bool { return b&m != 0 }

40
mode_test.go Normal file
View file

@ -0,0 +1,40 @@
package apg
import (
"testing"
)
func TestSetClearHasToggleMode(t *testing.T) {
tt := []struct {
name string
mode Mode
}{
{"ModeNumber", ModeNumber},
{"ModeLowerCase", ModeLowerCase},
{"ModeUpperCase", ModeUpperCase},
{"ModeSpecial", ModeSpecial},
{"ModeHumanReadable", ModeHumanReadable},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
var m Mode
m = SetMode(m, tc.mode)
if !HasMode(m, tc.mode) {
t.Errorf("SetMode() failed, mode not found in bitmask")
}
m = ToggleMode(m, tc.mode)
if HasMode(m, tc.mode) {
t.Errorf("ToggleMode() failed, mode found in bitmask")
}
m = ToggleMode(m, tc.mode)
if !HasMode(m, tc.mode) {
t.Errorf("ToggleMode() failed, mode not found in bitmask")
}
m = ClearMode(m, tc.mode)
if HasMode(m, tc.mode) {
t.Errorf("ClearMode() failed, mode found in bitmask")
}
})
}
}

View file

@ -9,25 +9,6 @@ import (
"strings"
)
const (
// CharRangeAlphaLower represents all lower-case alphabetical characters
CharRangeAlphaLower = "abcdefghijklmnopqrstuvwxyz"
// CharRangeAlphaLowerHuman represents the human-readable lower-case alphabetical characters
CharRangeAlphaLowerHuman = "abcdefghjkmnpqrstuvwxyz"
// CharRangeAlphaUpper represents all upper-case alphabetical characters
CharRangeAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// CharRangeAlphaUpperHuman represents the human-readable upper-case alphabetical characters
CharRangeAlphaUpperHuman = "ABCDEFGHJKMNPQRSTUVWXYZ"
// CharRangeNumber represents all numerical characters
CharRangeNumber = "1234567890"
// CharRangeNumberHuman represents all human-readable numerical characters
CharRangeNumberHuman = "23456789"
// CharRangeSpecial represents all special characters
CharRangeSpecial = `!\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~`
// CharRangeSpecialHuman represents all human-readable special characters
CharRangeSpecialHuman = `#%*+-:;=`
)
const (
// 7 bits to represent a letter index
letterIdxBits = 7
@ -64,10 +45,10 @@ func (g *Generator) RandomBytes(n int64) ([]byte, error) {
return b, nil
}
// RandomString returns a random string of length l based of the range of characters given.
// RandomStringFromCharRange returns a random string of length l based of the range of characters given.
// The method makes use of the crypto/random package and therfore is
// cryptographically secure
func (g *Generator) RandomString(l int, cr string) (string, error) {
func (g *Generator) RandomStringFromCharRange(l int, cr string) (string, error) {
if l < 1 {
return "", ErrInvalidLength
}
@ -85,7 +66,7 @@ func (g *Generator) RandomString(l int, cr string) (string, error) {
}
for i, c, r := l-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; {
if r == 0 {
_, err := rand.Read(rp)
_, err = rand.Read(rp)
if err != nil {
return rs.String(), err
}

View file

@ -7,7 +7,7 @@ import (
)
func TestGenerator_CoinFlip(t *testing.T) {
g := New()
g := New(NewConfig())
cf := g.CoinFlip()
if cf < 0 || cf > 1 {
t.Errorf("CoinFlip failed(), expected 0 or 1, got: %d", cf)
@ -15,7 +15,7 @@ func TestGenerator_CoinFlip(t *testing.T) {
}
func TestGenerator_CoinFlipBool(t *testing.T) {
g := New()
g := New(NewConfig())
gt := false
for i := 0; i < 500_000; i++ {
cf := g.CoinFlipBool()
@ -43,7 +43,7 @@ func TestGenerator_RandNum(t *testing.T) {
{"RandNum should fail on negative", -1, 0, 0, true},
}
g := New()
g := New(NewConfig())
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rn, err := g.RandNum(tc.v)
@ -78,7 +78,7 @@ func TestGenerator_RandomBytes(t *testing.T) {
{"-1 bytes of randomness", -1, true},
}
g := New()
g := New(NewConfig())
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rb, err := g.RandomBytes(tc.l)
@ -102,7 +102,7 @@ func TestGenerator_RandomBytes(t *testing.T) {
}
func TestGenerator_RandomString(t *testing.T) {
g := New()
g := New(NewConfig())
l := 32
tt := []struct {
name string
@ -133,15 +133,15 @@ func TestGenerator_RandomString(t *testing.T) {
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rs, err := g.RandomString(l, tc.cr)
rs, err := g.RandomStringFromCharRange(l, tc.cr)
if err != nil && !tc.sf {
t.Errorf("RandomString failed: %s", err)
t.Errorf("RandomStringFromCharRange failed: %s", err)
}
if len(rs) != l && !tc.sf {
t.Errorf("RandomString failed. Expected length: %d, got: %d", l, len(rs))
t.Errorf("RandomStringFromCharRange failed. Expected length: %d, got: %d", l, len(rs))
}
if strings.ContainsAny(rs, tc.nr) {
t.Errorf("RandomString failed. Unexpected character found in returned string: %s", rs)
t.Errorf("RandomStringFromCharRange failed. Unexpected character found in returned string: %s", rs)
}
})
}
@ -149,7 +149,7 @@ func TestGenerator_RandomString(t *testing.T) {
func BenchmarkGenerator_CoinFlip(b *testing.B) {
b.ReportAllocs()
g := New()
g := New(NewConfig())
for i := 0; i < b.N; i++ {
_ = g.CoinFlip()
}
@ -157,7 +157,7 @@ func BenchmarkGenerator_CoinFlip(b *testing.B) {
func BenchmarkGenerator_RandomBytes(b *testing.B) {
b.ReportAllocs()
g := New()
g := New(NewConfig())
var l int64 = 1024
for i := 0; i < b.N; i++ {
_, err := g.RandomBytes(l)
@ -170,12 +170,12 @@ func BenchmarkGenerator_RandomBytes(b *testing.B) {
func BenchmarkGenerator_RandomString(b *testing.B) {
b.ReportAllocs()
g := New()
g := New(NewConfig())
cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial
for i := 0; i < b.N; i++ {
_, err := g.RandomString(32, cr)
_, err := g.RandomStringFromCharRange(32, cr)
if err != nil {
b.Errorf("RandomString() failed: %s", err)
b.Errorf("RandomStringFromCharRange() failed: %s", err)
}
}
}