apg-go/random.go

132 lines
3.3 KiB
Go
Raw Normal View History

package apg
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"math/big"
"strings"
)
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
}
// 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) RandomStringFromCharRange(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
}
// GetPasswordLength returns the password length based on the given config
// parameters
func (g *Generator) GetPasswordLength() (int64, error) {
if g.config.FixedLength > 0 {
return g.config.FixedLength, nil
}
mil := g.config.MinLength
mal := g.config.MaxLength
if mil > mal {
mal = mil
}
diff := mal - mil + 1
ra, err := g.RandNum(diff)
if err != nil {
return 0, fmt.Errorf("failed to calculate password length: %w", err)
}
l := mil + ra
if l <= 0 {
return 1, nil
}
return l, nil
}