mirror of
https://github.com/wneessen/apg-go.git
synced 2024-11-09 15:52:54 +01:00
v2: Complete rework of the library and the client
This commit is contained in:
parent
2975dfea51
commit
e94b1ade5c
7 changed files with 367 additions and 0 deletions
16
.golangci.toml
Normal file
16
.golangci.toml
Normal 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
8
.idea/.gitignore
vendored
Normal 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
12
apg.go
Normal 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
20
cmd/apg/apg.go
Normal 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
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/wneessen/apg-go
|
||||||
|
|
||||||
|
go 1.20
|
127
random.go
Normal file
127
random.go
Normal 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
181
random_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue