2023-04-18 11:49:44 +02:00
|
|
|
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")
|
|
|
|
)
|
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-08-05 18:10:11 +02:00
|
|
|
// Generate generates a password based on all the different config flags and returns
|
|
|
|
// it as string type. If the generation fails, an error will be thrown
|
|
|
|
func (g *Generator) Generate() (string, error) {
|
2023-08-06 18:55:47 +02:00
|
|
|
switch g.config.Algorithm {
|
|
|
|
case AlgoCoinFlip:
|
|
|
|
return g.generateCoinFlip()
|
|
|
|
case AlgoRandom:
|
|
|
|
return g.generateRandom()
|
|
|
|
case AlgoUnsupported:
|
|
|
|
return "", fmt.Errorf("unsupported algorithm")
|
2023-08-05 18:10:11 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
return "", nil
|
|
|
|
}
|
2023-08-05 18:10:11 +02:00
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
// 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)
|
2023-08-05 18:10:11 +02:00
|
|
|
if err != nil {
|
2023-08-06 18:55:47 +02:00
|
|
|
return 0, err
|
2023-08-05 18:10:11 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
l := mil + ra
|
|
|
|
if l <= 0 {
|
|
|
|
return 1, nil
|
|
|
|
}
|
|
|
|
return l, nil
|
2023-08-05 18:10:11 +02:00
|
|
|
}
|
|
|
|
|
2023-04-18 11:49:44 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-08-04 16:02:58 +02:00
|
|
|
// RandomStringFromCharRange returns a random string of length l based of the range of characters given.
|
2023-04-18 11:49:44 +02:00
|
|
|
// The method makes use of the crypto/random package and therfore is
|
|
|
|
// cryptographically secure
|
2023-08-06 18:55:47 +02:00
|
|
|
func (g *Generator) RandomStringFromCharRange(l int64, cr string) (string, error) {
|
2023-04-18 11:49:44 +02:00
|
|
|
if l < 1 {
|
|
|
|
return "", ErrInvalidLength
|
|
|
|
}
|
|
|
|
if len(cr) < 1 {
|
|
|
|
return "", ErrInvalidCharRange
|
|
|
|
}
|
|
|
|
rs := strings.Builder{}
|
2023-08-06 18:55:47 +02:00
|
|
|
|
|
|
|
// As long as the length is smaller than the max. int32 value let's grow
|
|
|
|
// the string builder to the actual size, so we need less allocations
|
|
|
|
if l <= 2147483647 {
|
|
|
|
rs.Grow(int(l))
|
|
|
|
}
|
|
|
|
|
2023-04-18 11:49:44 +02:00
|
|
|
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 {
|
2023-08-04 16:02:58 +02:00
|
|
|
_, err = rand.Read(rp)
|
2023-04-18 11:49:44 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
// GetCharRangeFromConfig checks the Mode from the Config and returns a
|
|
|
|
// list of all possible characters that are supported by these Mode
|
|
|
|
func (g *Generator) GetCharRangeFromConfig() string {
|
|
|
|
cr := strings.Builder{}
|
|
|
|
if MaskHasMode(g.config.Mode, ModeLowerCase) {
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr.WriteString(CharRangeAlphaLowerHuman)
|
|
|
|
default:
|
|
|
|
cr.WriteString(CharRangeAlphaLower)
|
|
|
|
}
|
2023-04-18 11:49:44 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
if MaskHasMode(g.config.Mode, ModeNumeric) {
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr.WriteString(CharRangeNumericHuman)
|
|
|
|
default:
|
|
|
|
cr.WriteString(CharRangeNumeric)
|
|
|
|
}
|
2023-04-18 11:49:44 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
if MaskHasMode(g.config.Mode, ModeSpecial) {
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr.WriteString(CharRangeSpecialHuman)
|
|
|
|
default:
|
|
|
|
cr.WriteString(CharRangeSpecial)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if MaskHasMode(g.config.Mode, ModeUpperCase) {
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr.WriteString(CharRangeAlphaUpperHuman)
|
|
|
|
default:
|
|
|
|
cr.WriteString(CharRangeAlphaUpper)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cr.String()
|
2023-04-18 11:49:44 +02:00
|
|
|
}
|
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
func (g *Generator) checkMinimumRequirements(pw string) bool {
|
|
|
|
ok := true
|
|
|
|
if g.config.MinLowerCase > 0 {
|
|
|
|
var cr string
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr = CharRangeAlphaLowerHuman
|
|
|
|
default:
|
|
|
|
cr = CharRangeAlphaLower
|
|
|
|
}
|
2023-04-18 11:49:44 +02:00
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
m := 0
|
|
|
|
for _, c := range cr {
|
|
|
|
m += strings.Count(pw, string(c))
|
|
|
|
}
|
|
|
|
if int64(m) < g.config.MinLowerCase {
|
|
|
|
ok = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if g.config.MinNumeric > 0 {
|
|
|
|
var cr string
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr = CharRangeNumericHuman
|
|
|
|
default:
|
|
|
|
cr = CharRangeNumeric
|
|
|
|
}
|
2023-08-05 15:04:34 +02:00
|
|
|
|
2023-08-06 18:55:47 +02:00
|
|
|
m := 0
|
|
|
|
for _, c := range cr {
|
|
|
|
m += strings.Count(pw, string(c))
|
|
|
|
}
|
|
|
|
if int64(m) < g.config.MinNumeric {
|
|
|
|
ok = false
|
|
|
|
}
|
2023-08-05 15:04:34 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
if g.config.MinSpecial > 0 {
|
|
|
|
var cr string
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr = CharRangeSpecialHuman
|
|
|
|
default:
|
|
|
|
cr = CharRangeSpecial
|
|
|
|
}
|
|
|
|
|
|
|
|
m := 0
|
|
|
|
for _, c := range cr {
|
|
|
|
m += strings.Count(pw, string(c))
|
|
|
|
}
|
|
|
|
if int64(m) < g.config.MinSpecial {
|
|
|
|
ok = false
|
|
|
|
}
|
2023-08-05 15:04:34 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
if g.config.MinUpperCase > 0 {
|
|
|
|
var cr string
|
|
|
|
switch MaskHasMode(g.config.Mode, ModeHumanReadable) {
|
|
|
|
case true:
|
|
|
|
cr = CharRangeAlphaUpperHuman
|
|
|
|
default:
|
|
|
|
cr = CharRangeAlphaUpper
|
|
|
|
}
|
|
|
|
|
|
|
|
m := 0
|
|
|
|
for _, c := range cr {
|
|
|
|
m += strings.Count(pw, string(c))
|
|
|
|
}
|
|
|
|
if int64(m) < g.config.MinUpperCase {
|
|
|
|
ok = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateCoinFlip is executed when Generate() is called with Algorithm set
|
|
|
|
// to AlgoCoinFlip
|
|
|
|
func (g *Generator) generateCoinFlip() (string, error) {
|
|
|
|
switch g.CoinFlipBool() {
|
|
|
|
case true:
|
|
|
|
return "Heads", nil
|
|
|
|
default:
|
|
|
|
return "Tails", nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateRandom is executed when Generate() is called with Algorithm set
|
|
|
|
// to AlgoRandmom
|
|
|
|
func (g *Generator) generateRandom() (string, error) {
|
|
|
|
l, err := g.GetPasswordLength()
|
2023-08-05 15:04:34 +02:00
|
|
|
if err != nil {
|
2023-08-06 18:55:47 +02:00
|
|
|
return "", fmt.Errorf("failed to calculate password length: %w", err)
|
2023-08-05 15:04:34 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
cr := g.GetCharRangeFromConfig()
|
|
|
|
var pw string
|
|
|
|
var ok bool
|
|
|
|
for !ok {
|
|
|
|
pw, err = g.RandomStringFromCharRange(l, cr)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
ok = g.checkMinimumRequirements(pw)
|
2023-08-05 15:04:34 +02:00
|
|
|
}
|
2023-08-06 18:55:47 +02:00
|
|
|
|
|
|
|
return pw, nil
|
2023-08-05 15:04:34 +02:00
|
|
|
}
|