~seirdy/moac

ref: v0.2.0 moac/pwgen.go -rw-r--r-- 4.2 KiB
86be58a3Rohan Kumar Chore: add Makefile 9 months ago
                                                                                
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package moac

import (
	cryptoRand "crypto/rand"
	"errors"
	"fmt"
	"math"
	"math/big"
	"math/rand"
	"unicode/utf8"

	"git.sr.ht/~seirdy/moac/entropy"
)

func randRune(runes []rune) (rune, error) {
	i, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(len(runes))))
	if err != nil {
		return ' ', fmt.Errorf("randRune: %w", err)
	}

	return runes[i.Int64()], nil
}

func addRuneToPw(password *string, runes []rune) error {
	newChar, err := randRune(runes)
	if err != nil {
		return fmt.Errorf("genpw: %w", err)
	}

	*password += string(newChar)

	return nil
}

func shuffle(password string) string {
	runified := []rune(password)
	rand.Shuffle(len(runified), func(i, j int) {
		runified[i], runified[j] = runified[j], runified[i]
	})

	return string(runified)
}

var errInvalidLenBounds = errors.New("bad length bounds")

func computePasswordLength(charsetSize int, pwEntropy float64, minLen, maxLen int) (int, error) {
	if maxLen > 0 && minLen > maxLen {
		return 0, fmt.Errorf("%w: maxLen can't be less than minLen", errInvalidLenBounds)
	}
	// combinations is 2^entropy, or 2^s
	// password length estimate is the logarithm of that with base charsetSize
	// logn(2^s) = s*logn(2) = s/log2(n)
	length := int(math.Ceil(pwEntropy / math.Log2(float64(charsetSize))))
	if length < minLen {
		length = minLen
	}

	if maxLen > 0 && length > maxLen {
		length = maxLen
	}

	return length, nil
}

func genpwFromGivenCharsets(charsetsGiven [][]rune, entropyWanted float64, minLen, maxLen int) (string, error) {
	var charsToPickFrom, pw string

	// at least one element from each charset
	for _, charset := range charsetsGiven {
		charsToPickFrom += string(charset)

		if err := addRuneToPw(&pw, charset); err != nil {
			return pw, fmt.Errorf("genpw: %w", err)
		}
	}

	runesToPickFrom := []rune(charsToPickFrom)
	// figure out the minimum acceptable length of the password and fill that up before measuring entropy.
	pwLength, err := computePasswordLength(len(runesToPickFrom), entropyWanted, minLen, maxLen)
	if err != nil {
		return "", fmt.Errorf("can't generate password: %w", err)
	}

	for utf8.RuneCountInString(pw) < pwLength {
		err := addRuneToPw(&pw, runesToPickFrom)
		if err != nil {
			return pw, fmt.Errorf("genpw: %w", err)
		}
	}

	for maxLen == 0 || utf8.RuneCountInString(pw) < maxLen {
		err := addRuneToPw(&pw, runesToPickFrom)
		if err != nil {
			return pw, fmt.Errorf("genpw: %w", err)
		}

		computedEntropy, err := entropy.Entropy(pw)
		if err != nil || entropyWanted < computedEntropy {
			return shuffle(pw), err
		}
	}

	return shuffle(pw), nil
}

func buildCharsets(charsetsEnumerated *[]string) [][]rune {
	var charsetsGiven [][]rune

	for _, charset := range *charsetsEnumerated {
		charsetRunes, found := entropy.Charsets[charset]

		switch {
		case found:
			charsetsGiven = append(charsetsGiven, charsetRunes)
		case charset == "latin":
			charsetsGiven = append(
				charsetsGiven,
				entropy.Charsets["latinExtendedA"], entropy.Charsets["latinExtendedB"], entropy.Charsets["ipaExtensions"],
			)
		default:
			charsetsGiven = append(charsetsGiven, []rune(charset))
		}
	}

	return charsetsGiven
}

// GenPW generates a random password using characters from the charsets enumerated by charsetsWanted.
// At least one element of each charset is used.
// Available charsets include "lowercase", "uppercase", "numbers", "symbols",
// "latinExtendedA", "latinExtendedB", and "ipaExtensions". "latin" is also available
// and is equivalent to specifying "latinExtendedA latinExtendedB ipaExtensions".
// Anything else will be treated as a string containing runes of a new custom charset
// to use.
// If entropyWanted is 0, the generated password has at least 256 bits of entropy;
// otherwise, it has entropyWanted bits of entropy.
// minLen and maxLen are ignored when set to zero; otherwise, they set lower/upper
// bounds on password character count and override entropyWanted if necessary.
func GenPW(charsetsEnumerated []string, entropyWanted float64, minLen, maxLen int) (string, error) {
	charsetsGiven := buildCharsets(&charsetsEnumerated)
	if entropyWanted == 0 {
		return genpwFromGivenCharsets(charsetsGiven, 256, minLen, maxLen)
	}

	return genpwFromGivenCharsets(charsetsGiven, entropyWanted, minLen, maxLen)
}