summaryrefslogtreecommitdiffstats
path: root/internal/passgen/passgen.go
blob: 1c3241dd2e2e8d0ee6f69d1f8c4054046a00a3b7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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
}