summaryrefslogtreecommitdiffstats
path: root/internal/passgen/passgen.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/passgen/passgen.go')
-rw-r--r--internal/passgen/passgen.go124
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
+}