Merge pull request #60 from wneessen/binary_random

Add binary mode for secret generation
This commit is contained in:
Winni Neessen 2024-03-17 18:26:03 +01:00 committed by GitHub
commit 9f035c5834
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 162 additions and 7 deletions

View file

@ -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 <length>`: The minimum length of the password to be generated (Default: 12)
- `-x <length>`: The maximum length of the password to be generated (Default: 20)
- `-f <length>`: Fixed length of the password to be generated (Ignores -m and -x)

View file

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

View file

@ -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) {

2
apg.go
View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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{"ä"},