mirror of
https://github.com/wneessen/apg-go.git
synced 2024-12-23 03:30:39 +01:00
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:
parent
e94b1ade5c
commit
ac97b94ec9
10 changed files with 366 additions and 46 deletions
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal 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
29
algo.go
Normal 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
11
apg.go
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
67
config.go
Normal 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
69
config_test.go
Normal 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
51
mode.go
Normal 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
40
mode_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
25
random.go
25
random.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue