// SPDX-FileCopyrightText: 2021-2024 Winni Neessen // // SPDX-License-Identifier: MIT package apg import ( "crypto/rand" "encoding/binary" "errors" "fmt" "math/big" "os" "regexp" "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 } minLength := g.config.MinLength maxLength := g.config.MaxLength if minLength > maxLength { maxLength = minLength } diff := maxLength - minLength + 1 randNum, err := g.RandNum(diff) if err != nil { return 0, err } length := minLength + randNum if length <= 0 { return 1, nil } return length, nil } // RandomBytes returns a byte slice of random bytes with given length that got generated by // the crypto/rand generator func (g *Generator) RandomBytes(length int64) ([]byte, error) { if length < 1 { return nil, ErrInvalidLength } bytes := make([]byte, length) numBytes, err := rand.Read(bytes) if int64(numBytes) != length { return nil, ErrLengthMismatch } if err != nil { return nil, err } return bytes, nil } // RandNum generates a random, non-negative number with given maximum value func (g *Generator) RandNum(max int64) (int64, error) { if max < 1 { return 0, ErrInvalidLength } max64 := big.NewInt(max) randNum, err := rand.Int(rand.Reader, max64) if err != nil { return 0, fmt.Errorf("random number generation failed: %w", err) } return randNum.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(length int64, charRange string) (string, error) { if length < 1 { return "", ErrInvalidLength } if len(charRange) < 1 { return "", ErrInvalidCharRange } randString := 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 length <= maxInt32 { randString.Grow(int(length)) } charRangeLength := len(charRange) randPool := make([]byte, 8) _, err := rand.Read(randPool) if err != nil { return randString.String(), err } for idx, char, rest := length-1, binary.BigEndian.Uint64(randPool), letterIdxMax; idx >= 0; { if rest == 0 { _, err = rand.Read(randPool) if err != nil { return randString.String(), err } char, rest = binary.BigEndian.Uint64(randPool), letterIdxMax } if i := int(char & letterIdxMask); i < charRangeLength { randString.WriteByte(charRange[i]) idx-- } char >>= letterIdxBits rest-- } return randString.String(), nil } // checkMinimumRequirements checks if a password meets the minimum requirements specified in the // generator's configuration. It returns true if the password meets the requirements, otherwise it // returns false. // // The minimum requirements for each character type (lowercase, numeric, special, uppercase) are // checked independently. For each character type, the corresponding character range is determined // based on the generator's configuration. The password is then checked for the presence of each // character in the character range, and a count is maintained. func (g *Generator) checkMinimumRequirements(password string) bool { ok := true if g.config.MinLowerCase > 0 { var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: charRange = CharRangeAlphaLowerHuman default: charRange = CharRangeAlphaLower } matchesMinimumAmount(charRange, password, g.config.MinLowerCase, &ok) } if g.config.MinNumeric > 0 { var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: charRange = CharRangeNumericHuman default: charRange = CharRangeNumeric } matchesMinimumAmount(charRange, password, g.config.MinNumeric, &ok) } if g.config.MinSpecial > 0 { var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: charRange = CharRangeSpecialHuman default: charRange = CharRangeSpecial } matchesMinimumAmount(charRange, password, g.config.MinSpecial, &ok) } if g.config.MinUpperCase > 0 { var charRange string switch MaskHasMode(g.config.Mode, ModeHumanReadable) { case true: charRange = CharRangeAlphaUpperHuman default: charRange = CharRangeAlphaUpper } matchesMinimumAmount(charRange, password, g.config.MinUpperCase, &ok) } return ok } // generateCoinFlip is executed when Generate() is called with Algorithm set // to AlgoCoinFlip func (g *Generator) generateCoinFlip() (string, error) { if g.CoinFlipBool() { return "Heads", nil } return "Tails", nil } // generatePronounceable is executed when Generate() is called with Algorithm set // to AlgoPronounceable func (g *Generator) generatePronounceable() (string, error) { var password string g.syllables = make([]string, 0) length, err := g.GetPasswordLength() if err != nil { return "", fmt.Errorf("failed to calculate password length: %w", err) } characterSet := KoremutakeSyllables characterSet = append(characterSet, strings.Split(CharRangeNumericHuman, "")...) characterSet = append(characterSet, strings.Split(CharRangeSpecialHuman, "")...) characterSetLength := len(characterSet) for int64(len(password)) < length { randNum, err := g.RandNum(int64(characterSetLength)) if err != nil { return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %w", err) } nextSyllable := characterSet[randNum] if g.CoinFlipBool() { syllableLength := len(nextSyllable) characterPosition, err := g.RandNum(int64(syllableLength)) if err != nil { return "", fmt.Errorf("failed to generate a random number for Koremutake syllable generation: %w", err) } randomChar := string(nextSyllable[characterPosition]) nextSyllable = strings.ReplaceAll(nextSyllable, randomChar, strings.ToUpper(randomChar)) } password += nextSyllable g.syllables = append(g.syllables, nextSyllable) } return password, nil } // generateBinary is executed when Generate() is called with Algorithm set // to AlgoBinary func (g *Generator) generateBinary() (string, error) { length := DefaultBinarySize if g.config.FixedLength > 0 { length = g.config.FixedLength } randBytes := make([]byte, length) _, err := rand.Read(randBytes) if err != nil { return "", fmt.Errorf("failed to generate random bytes: %w", err) } if g.config.BinaryHexMode { return fmt.Sprintf("%x", randBytes), nil } return string(randBytes), nil } // generateRandom is executed when Generate() is called with Algorithm set // to AlgoRandom func (g *Generator) generateRandom() (string, error) { length, err := g.GetPasswordLength() if err != nil { return "", fmt.Errorf("failed to calculate password length: %w", err) } charRange := g.GetCharRangeFromConfig() var password string var ok bool for !ok { password, err = g.RandomStringFromCharRange(length, charRange) if err != nil { return "", err } ok = g.checkMinimumRequirements(password) } if g.config.MobileGrouping { return GroupCharsForMobile(password), nil } return password, nil } // matchesMinimumAmount checks if the number of occurrences of characters in // charRange in the password is less than minAmount and updates the // value of ok accordingly. func matchesMinimumAmount(charRange, password string, minAmount int64, ok *bool) { count := 0 for _, char := range charRange { count += strings.Count(password, string(char)) } if int64(count) < minAmount { *ok = false } }