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< 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, err } l := mil + ra if l <= 0 { return 1, nil } return l, nil } // 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 } // 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 } // 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 int64, cr string) (string, error) { if l < 1 { return "", ErrInvalidLength } if len(cr) < 1 { return "", ErrInvalidCharRange } rs := strings.Builder{} // 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)) } 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 } // 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) } } if MaskHasMode(g.config.Mode, ModeNumeric) { switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: cr.WriteString(CharRangeNumericHuman) default: cr.WriteString(CharRangeNumeric) } } 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() } 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 } 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 } m := 0 for _, c := range cr { m += strings.Count(pw, string(c)) } if int64(m) < g.config.MinNumeric { ok = false } } 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 } } 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() if err != nil { return "", fmt.Errorf("failed to calculate password length: %w", err) } 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) } return pw, nil }