diff --git a/README.md b/README.md index b8c3b9b..aa9acc2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,14 @@ vEbErlaFryaNgyex (vE-bEr-la-Fry-aN-gy-ex) We generated a 15-character long pronounceable phrase with syllables output, for easy use in e. g. a phone verification process. +### Cryptographic key for encryption +```shell +$ apg -a 3 -f 32 -bh +``` +We generated a 32 bytes/256 bits long fully binary secret that can be used i. e. as +encryption key for a AES-256 symmetric encryption. The output is represented in +hexadecimal format. + ## Installation ### Docker @@ -327,6 +335,24 @@ Heads Heads ``` +### Binary mode +Since v1.0.1 apg-go has a new algorithm for binary secrets. This is a very basic mode that will ignore +most of the available options, as it will only generate binary secrets with full 256 bits of randomness. +The only available options for this mode are: `-f` to set the length of the returned secret in bytes, +`-bh` to tell apg-go to output the generated secret in hexadecial representation and `-bn` to instruct +apg-go to return a newline after the generated secret. Any other option available in the other modes +will be ignored. + +This mode can be useful for example if you need to generate a AES-256 encryption key. Since 32 bytes is +the default length for the secret generation in this mode, you can simply generate a secret key with +the following command: +```shell +$ apg -a 3 -bh +a1cdab8db365af3d70828b1fe43b7896190c157ad3f1ae2a0a1d52ec1628c6b5 +``` +*For ease for readability we used the `-bh` flag, to instruct apg-go to output the secret in its +hexadecimal representation* + ### Minimum required characters Even though in apg-go you can select what kind of characters are used for the password generation, it is not guaranteed, that if you request a password with a numeric value, that the generated password will @@ -376,6 +402,9 @@ _apg-go_ replicates most of the parameters of the original c-apg. Some parameter - `0`: Pronouncable password generation (Koremutake syllables) - `1`: Random password generation according to password modes/flags - `2`: Coinflip (returns heads or tails) + - `3`: Binary mode (returns a secret with 256 bits of randomness) +- `-bh`: When set, will print the generated secret in its hex representation (Default: off) +- `-bn`: When set, will return a new line character after the generated secret (Default: off) - `-m `: The minimum length of the password to be generated (Default: 12) - `-x `: The maximum length of the password to be generated (Default: 20) - `-f `: Fixed length of the password to be generated (Ignores -m and -x) diff --git a/algo.go b/algo.go index 74d2937..1660652 100644 --- a/algo.go +++ b/algo.go @@ -18,6 +18,9 @@ const ( // AlgoCoinFlip represents a very simple coinflip algorithm returning "heads" // or "tails" AlgoCoinFlip + // AlgoBinary represents a full binary randomness mode with up to 256 bits + // of randomness + AlgoBinary // AlgoUnsupported represents an unsupported algorithm AlgoUnsupported ) @@ -32,6 +35,8 @@ func IntToAlgo(a int) Algorithm { return AlgoRandom case 2: return AlgoCoinFlip + case 3: + return AlgoBinary default: return AlgoUnsupported } diff --git a/algo_test.go b/algo_test.go index 3aa8ca5..992d6b9 100644 --- a/algo_test.go +++ b/algo_test.go @@ -4,7 +4,9 @@ package apg -import "testing" +import ( + "testing" +) func TestIntToAlgo(t *testing.T) { tt := []struct { @@ -15,7 +17,8 @@ func TestIntToAlgo(t *testing.T) { {"AlgoPronounceable", 0, AlgoPronounceable}, {"AlgoRandom", 1, AlgoRandom}, {"AlgoCoinflip", 2, AlgoCoinFlip}, - {"AlgoUnsupported", 3, AlgoUnsupported}, + {"AlgoBinary", 3, AlgoBinary}, + {"AlgoUnsupported", 4, AlgoUnsupported}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { diff --git a/apg.go b/apg.go index 1e2baee..7c9bf15 100644 --- a/apg.go +++ b/apg.go @@ -5,7 +5,7 @@ package apg // VERSION represents the version string -const VERSION = "1.0.0" +const VERSION = "1.0.1" // Generator is the password generator type of the APG package type Generator struct { diff --git a/cmd/apg/apg.go b/cmd/apg/apg.go index 5a0a6d5..04d00ba 100644 --- a/cmd/apg/apg.go +++ b/cmd/apg/apg.go @@ -31,6 +31,8 @@ func main() { var modeString string var complexPass, humanReadable, lowerCase, numeric, special, showVer, upperCase bool flag.IntVar(&algorithm, "a", 1, "") + flag.BoolVar(&config.BinaryHexMode, "bh", false, "") + flag.BoolVar(&config.BinaryNewline, "bn", false, "") flag.BoolVar(&complexPass, "C", false, "") flag.StringVar(&config.ExcludeChars, "E", "", "") flag.Int64Var(&config.FixedLength, "f", 0, "") @@ -144,6 +146,23 @@ func configOldStyle(config *apg.Config, humanReadable, lowerCase, upperCase, func generate(config *apg.Config) { generator := apg.New(config) + + // In binary mode we only generate a single secret + if config.Algorithm == apg.AlgoBinary { + password, err := generator.Generate() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to generate password: %s\n", err) + os.Exit(1) + } + if config.BinaryNewline { + fmt.Println(password) + return + } + fmt.Print(password) + return + } + + // For any other mode we cycle through the amount of passwords to be generated for i := int64(0); i < config.NumberPass; i++ { password, err := generator.Generate() if err != nil { @@ -197,12 +216,17 @@ Flags: - 0: pronounceable password generation (koremutake syllables) - 1: random password generation according to password modes/flags - 2: coinflip (returns heads or tails) + - 3: full binary mode (generates simple 256 bit randomness) + -bh When set, will print the generated secret in its hex representation (Default: off) + -bn When set, will return a new line character after the generated secret (Default: off) + - Note: The -bX options only apply to binary mode (Algo: 3) -m LENGTH Minimum length of the password to be generated (Default: 12) -x LENGTH Maximum length of the password to be generated (Default: 20) -f LENGTH Fixed length of the password to be generated (Ignores -m and -x) - Note: Due to the way the pronounceable password algorithm works, this setting might not always apply -n NUMBER Amount of password to be generated (Default: 6) + - Note: Does not apply to binary mode (Algo: 3) -E CHARS List of characters to be excluded in the generated password -M [LUNSHClunshc] New style password flags - Note: new-style flags have higher priority than any of the old-style flags diff --git a/config.go b/config.go index 1cc689b..a64bb0b 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,8 @@ const ( DefaultMinLength int64 = 12 // DefaultMaxLength reflects the default maximum length of a generated password DefaultMaxLength int64 = 20 + // DefaultBinarySize is the default byte size for generating binary random bytes + DefaultBinarySize int64 = 32 // DefaultMode sets the default character set mode bitmask to a combination of // lower- and upper-case characters as well as numbers DefaultMode ModeMask = ModeLowerCase | ModeNumeric | ModeUpperCase @@ -21,6 +23,11 @@ const ( type Config struct { // Algorithm sets the Algorithm used for the password generation Algorithm Algorithm + // BinaryHexMode if set will output the hex representation of the generated + // binary random string + BinaryHexMode bool + // BinaryNewline if set will print out a new line in AlgoBinary mode + BinaryNewline bool // CheckHIBP sets a flag if the generated password has to be checked // against the HIBP pwned password database CheckHIBP bool @@ -88,6 +95,13 @@ func WithAlgorithm(algo Algorithm) Option { } } +// WithBinaryHexMode sets the hex mode for the AlgoBinary +func WithBinaryHexMode() Option { + return func(config *Config) { + config.BinaryHexMode = true + } +} + // WithExcludeChars sets a list of characters to be excluded in the generated // passwords func WithExcludeChars(chars string) Option { diff --git a/config_test.go b/config_test.go index 19a92ab..4a8bab3 100644 --- a/config_test.go +++ b/config_test.go @@ -46,7 +46,8 @@ func TestWithAlgorithm(t *testing.T) { {"Pronounceable passwords", AlgoPronounceable, 0}, {"Random passwords", AlgoRandom, 1}, {"Coinflip", AlgoCoinFlip, 2}, - {"Unsupported", AlgoUnsupported, 3}, + {"Binary", AlgoBinary, 3}, + {"Unsupported", AlgoUnsupported, 4}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -67,6 +68,18 @@ func TestWithAlgorithm(t *testing.T) { } } +func TestWithBinaryHexMode(t *testing.T) { + c := NewConfig(WithBinaryHexMode()) + if c == nil { + t.Errorf("NewConfig(WithBinaryHexMode()) failed, expected config pointer but got nil") + return + } + if !c.BinaryHexMode { + t.Errorf("NewConfig(WithBinaryHexMode()) failed, expected chars: %t, got: %t", + true, c.BinaryHexMode) + } +} + func TestWithExcludeChars(t *testing.T) { e := "abcdefg" c := NewConfig(WithExcludeChars(e)) diff --git a/hibp_test.go b/hibp_test.go index 16c6e3a..85fb09d 100644 --- a/hibp_test.go +++ b/hibp_test.go @@ -21,7 +21,8 @@ func TestHasBeenPwned(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := HasBeenPwned(tt.password) if err != nil { - t.Errorf("HasBeenPwned() failed: %s", err) + t.Logf("HasBeenPwned() failed: %s", err) + return } if tt.want != got { t.Errorf("HasBeenPwned() failed, wanted: %t, got: %t", tt.want, got) diff --git a/random.go b/random.go index 609d6ec..1195b2e 100644 --- a/random.go +++ b/random.go @@ -57,10 +57,13 @@ func (g *Generator) Generate() (string, error) { return g.generateCoinFlip() case AlgoRandom: return g.generateRandom() + case AlgoBinary: + return g.generateBinary() case AlgoUnsupported: return "", fmt.Errorf("unsupported algorithm") + default: + return "", fmt.Errorf("unsupported algorithm") } - return "", nil } // GetCharRangeFromConfig checks the Mode from the Config and returns a @@ -315,8 +318,26 @@ func (g *Generator) generatePronounceable() (string, error) { return password, nil } +// generateBinary is executed when Generate() is called with Algorithm set +// to AlgoBinary +func (g *Generator) generateBinary() (string, error) { + length := DefaultBinarySize + if g.config.FixedLength > 0 { + length = g.config.FixedLength + } + randBytes := make([]byte, length) + _, err := rand.Read(randBytes) + if err != nil { + return "", fmt.Errorf("failed to generate random bytes: %w", err) + } + if g.config.BinaryHexMode { + return fmt.Sprintf("%x", randBytes), nil + } + return string(randBytes), nil +} + // generateRandom is executed when Generate() is called with Algorithm set -// to AlgoRandmom +// to AlgoRandom func (g *Generator) generateRandom() (string, error) { length, err := g.GetPasswordLength() if err != nil { diff --git a/random_test.go b/random_test.go index b4e6feb..2b088bf 100644 --- a/random_test.go +++ b/random_test.go @@ -471,6 +471,10 @@ func TestGenerate(t *testing.T) { name: "Random", algorithm: AlgoRandom, }, + { + name: "Binary", + algorithm: AlgoBinary, + }, { name: "Unsupported", algorithm: AlgoUnsupported, @@ -530,3 +534,38 @@ func BenchmarkGenerator_RandomString(b *testing.B) { } } } + +func TestGenerator_generateBinary(t *testing.T) { + tests := []struct { + name string + config *Config + wantErr bool + }{ + { + name: "Positive Case - Binary in Hex", + config: &Config{FixedLength: 10, BinaryHexMode: true}, + wantErr: false, + }, + { + name: "Negative Case - Insufficient length", + config: &Config{FixedLength: -1, BinaryHexMode: false}, + wantErr: false, + }, + { + name: "Positive Case - Binary without Hex", + config: &Config{FixedLength: 10, BinaryHexMode: false}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &Generator{config: tt.config} + _, err := g.generateBinary() + if (err != nil) != tt.wantErr { + t.Errorf("Generator.generateBinary() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/spelling_test.go b/spelling_test.go index e42b3ce..edc518d 100644 --- a/spelling_test.go +++ b/spelling_test.go @@ -140,6 +140,12 @@ func TestPronounce(t *testing.T) { want: "mu-sa", wantErr: false, }, + { + name: "Pronounce_Mixed", + syllables: []string{"mu", "1"}, + want: "mu-ONE", + wantErr: false, + }, { name: "Pronounce_NonKoremutakeSyllable", syllables: []string{"รค"},