v2: Complete rework of the library and the client

This commit is contained in:
Winni Neessen 2023-04-18 11:49:44 +02:00
parent 2975dfea51
commit e94b1ade5c
Signed by: wneessen
GPG key ID: 385AC9889632126E
7 changed files with 367 additions and 0 deletions

16
.golangci.toml Normal file
View file

@ -0,0 +1,16 @@
## SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
##
## SPDX-License-Identifier: MIT
[run]
go = "1.20"
tests = true
skip-dirs = ["ui/"]
[linters]
enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder",
"errname", "errorlint", "gofmt", "gofumpt", "goimports"]
[linters-settings.goimports]
local-prefixes = "git.cgn.cleverbridge.com/infosec/vulnmon"

8
.idea/.gitignore vendored Normal file
View file

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

12
apg.go Normal file
View file

@ -0,0 +1,12 @@
package apg
// Generator is the password generator type of the APG package
type Generator struct {
// charRange is the range of character used for the
charRange string
}
// New returns a new password Generator type
func New() *Generator {
return &Generator{}
}

20
cmd/apg/apg.go Normal file
View file

@ -0,0 +1,20 @@
// Package main is the APG command line client that makes use of the apg-go library
package main
import (
"fmt"
"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)
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/wneessen/apg-go
go 1.20

127
random.go Normal file
View file

@ -0,0 +1,127 @@
package apg
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"math/big"
"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
// All 1-bits, as many as letterIdxBits
letterIdxMask = 1<<letterIdxBits - 1
// # of letter indices fitting in 63 bits)
letterIdxMax = 63 / letterIdxBits
)
var (
// ErrInvalidLength is returned if the provided maximum number is equal or less than zero
ErrInvalidLength = errors.New("provided length value cannot be zero or less")
// ErrLengthMismatch is returned if the number of generated bytes does not match the expected length
ErrLengthMismatch = errors.New("number of generated random bytes does not match the expected length")
// ErrInvalidCharRange is returned if the given range of characters is not valid
ErrInvalidCharRange = errors.New("provided character range is not valid or empty")
)
// RandomBytes returns a byte slice of random bytes with length n that got generated by
// the crypto/rand generator
func (g *Generator) RandomBytes(n int64) ([]byte, error) {
if n < 1 {
return nil, ErrInvalidLength
}
b := make([]byte, n)
l, err := rand.Read(b)
if int64(l) != n {
return nil, ErrLengthMismatch
}
if err != nil {
return nil, err
}
return b, nil
}
// RandomString 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) {
if l < 1 {
return "", ErrInvalidLength
}
if len(cr) < 1 {
return "", ErrInvalidCharRange
}
rs := strings.Builder{}
rs.Grow(l)
crl := len(cr)
rp := make([]byte, 8)
_, err := rand.Read(rp)
if err != nil {
return rs.String(), err
}
for i, c, r := l-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; {
if r == 0 {
_, err := rand.Read(rp)
if err != nil {
return rs.String(), err
}
c, r = binary.BigEndian.Uint64(rp), letterIdxMax
}
if idx := int(c & letterIdxMask); idx < crl {
rs.WriteByte(cr[idx])
i--
}
c >>= letterIdxBits
r--
}
return rs.String(), nil
}
// RandNum generates a random, non-negative number with given maximum value
func (g *Generator) RandNum(m int64) (int64, error) {
if m < 1 {
return 0, ErrInvalidLength
}
mbi := big.NewInt(m)
rn, err := rand.Int(rand.Reader, mbi)
if err != nil {
return 0, fmt.Errorf("random number generation failed: %w", err)
}
return rn.Int64(), nil
}
// CoinFlip performs a simple coinflip based on the rand library and returns 1 or 0
func (g *Generator) CoinFlip() int64 {
cf, _ := g.RandNum(2)
return cf
}
// CoinFlipBool performs a simple coinflip based on the rand library and returns true or false
func (g *Generator) CoinFlipBool() bool {
return g.CoinFlip() == 1
}

181
random_test.go Normal file
View file

@ -0,0 +1,181 @@
package apg
import (
"bytes"
"strings"
"testing"
)
func TestGenerator_CoinFlip(t *testing.T) {
g := New()
cf := g.CoinFlip()
if cf < 0 || cf > 1 {
t.Errorf("CoinFlip failed(), expected 0 or 1, got: %d", cf)
}
}
func TestGenerator_CoinFlipBool(t *testing.T) {
g := New()
gt := false
for i := 0; i < 500_000; i++ {
cf := g.CoinFlipBool()
if cf {
gt = true
break
}
}
if !gt {
t.Error("CoinFlipBool likely not working, expected at least one true value in 500k tries, got none")
}
}
func TestGenerator_RandNum(t *testing.T) {
tt := []struct {
name string
v int64
max int64
min int64
sf bool
}{
{"RandNum up to 1000", 1000, 1000, 0, false},
{"RandNum should be 1", 1, 1, 0, false},
{"RandNum should fail on 1", 0, 0, 0, true},
{"RandNum should fail on negative", -1, 0, 0, true},
}
g := New()
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rn, err := g.RandNum(tc.v)
if err == nil && tc.sf {
t.Errorf("random number generation was supposed to fail, but didn't, got: %d", rn)
}
if err != nil && !tc.sf {
t.Errorf("random number generation failed: %s", err)
}
if rn > tc.max {
t.Errorf("random number generation returned too big number expected below: %d, got: %d",
tc.max, rn)
}
if rn < tc.min {
t.Errorf("random number generation returned too small number, expected min: %d, got: %d",
tc.min, rn)
}
})
}
}
func TestGenerator_RandomBytes(t *testing.T) {
tt := []struct {
name string
l int64
sf bool
}{
{"1 bytes of randomness", 1, false},
{"100 bytes of randomness", 100, false},
{"1024 bytes of randomness", 1024, false},
{"4096 bytes of randomness", 4096, false},
{"-1 bytes of randomness", -1, true},
}
g := New()
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rb, err := g.RandomBytes(tc.l)
if err != nil && !tc.sf {
t.Errorf("random byte generation failed: %s", err)
return
}
if tc.sf {
return
}
bl := len(rb)
if int64(bl) != tc.l {
t.Errorf("lenght of provided bytes does not match requested length: got: %d, expected: %d",
bl, tc.l)
}
if bytes.Equal(rb, make([]byte, tc.l)) {
t.Errorf("random byte generation failed. returned slice is empty")
}
})
}
}
func TestGenerator_RandomString(t *testing.T) {
g := New()
l := 32
tt := []struct {
name string
cr string
nr string
sf bool
}{
{
"CharRange:AlphaLower", CharRangeAlphaLower,
CharRangeAlphaUpper + CharRangeNumber + CharRangeSpecial, false,
},
{
"CharRange:AlphaUpper", CharRangeAlphaUpper,
CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial, false,
},
{
"CharRange:Number", CharRangeNumber,
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeSpecial, false,
},
{
"CharRange:Special", CharRangeSpecial,
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumber, false,
},
{
"CharRange:Invalid", "",
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumber + CharRangeSpecial, true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rs, err := g.RandomString(l, tc.cr)
if err != nil && !tc.sf {
t.Errorf("RandomString failed: %s", err)
}
if len(rs) != l && !tc.sf {
t.Errorf("RandomString 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)
}
})
}
}
func BenchmarkGenerator_CoinFlip(b *testing.B) {
b.ReportAllocs()
g := New()
for i := 0; i < b.N; i++ {
_ = g.CoinFlip()
}
}
func BenchmarkGenerator_RandomBytes(b *testing.B) {
b.ReportAllocs()
g := New()
var l int64 = 1024
for i := 0; i < b.N; i++ {
_, err := g.RandomBytes(l)
if err != nil {
b.Errorf("failed to generate random bytes: %s", err)
return
}
}
}
func BenchmarkGenerator_RandomString(b *testing.B) {
b.ReportAllocs()
g := New()
cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumber + CharRangeSpecial
for i := 0; i < b.N; i++ {
_, err := g.RandomString(32, cr)
if err != nil {
b.Errorf("RandomString() failed: %s", err)
}
}
}