diff options
Diffstat (limited to 'internal/passgen/passgen.go')
| -rw-r--r-- | internal/passgen/passgen.go | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/internal/passgen/passgen.go b/internal/passgen/passgen.go new file mode 100644 index 0000000..1c3241d --- /dev/null +++ b/internal/passgen/passgen.go @@ -0,0 +1,124 @@ +package passgen + +import ( + "crypto/rand" + "errors" + "fmt" + "io" + "math" +) + +const ( + lowerChars = "abcdefghijklmnopqrstuvwxyz" + upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + digitChars = "0123456789" + symbolChars = "!@#$%^&*()-_=+[]{};:,.?/" +) + +var ErrEmptyAlphabet = errors.New("alphabet is empty") + +type Options struct { + Length int + UseLower bool + UseUpper bool + UseDigits bool + UseSymbols bool + Random io.Reader +} + +func DefaultOptions() Options { + return Options{ + Length: 24, + UseLower: true, + UseUpper: true, + UseDigits: true, + UseSymbols: true, + } +} + +func Generate(opts Options) (string, error) { + if opts.Length <= 0 { + return "", fmt.Errorf("length must be positive") + } + + alphabet := buildAlphabet(opts) + if alphabet == "" { + return "", ErrEmptyAlphabet + } + + random := opts.Random + if random == nil { + random = rand.Reader + } + + password := make([]byte, opts.Length) + for i := range password { + n, err := randomIndex(len(alphabet), random) + if err != nil { + return "", fmt.Errorf("choose password character: %w", err) + } + password[i] = alphabet[n] + } + + return string(password), nil +} + +func EntropyBits(length int, alphabetSize int) (float64, error) { + if length <= 0 { + return 0, fmt.Errorf("length must be positive") + } + if alphabetSize <= 1 { + return 0, fmt.Errorf("alphabet size must be greater than 1") + } + + return float64(length) * math.Log2(float64(alphabetSize)), nil +} + +func AlphabetSize(opts Options) int { + return len(buildAlphabet(opts)) +} + +func buildAlphabet(opts Options) string { + alphabet := "" + if opts.UseLower { + alphabet += lowerChars + } + if opts.UseUpper { + alphabet += upperChars + } + if opts.UseDigits { + alphabet += digitChars + } + if opts.UseSymbols { + alphabet += symbolChars + } + return alphabet +} + +func randomIndex(size int, random io.Reader) (int, error) { + if size <= 0 { + return 0, ErrEmptyAlphabet + } + if size > 256 { + return 0, fmt.Errorf("alphabet size %d exceeds supported maximum 256", size) + } + + cutoff := 256 - (256 % size) + for { + value, err := randomByte(random) + if err != nil { + return 0, err + } + if int(value) < cutoff { + return int(value) % size, nil + } + } +} + +func randomByte(random io.Reader) (byte, error) { + var b [1]byte + if _, err := io.ReadFull(random, b[:]); err != nil { + return 0, fmt.Errorf("read random byte: %w", err) + } + return b[0], nil +} |
