-
Notifications
You must be signed in to change notification settings - Fork 22
/
word_gen.go
282 lines (242 loc) · 8.55 KB
/
word_gen.go
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package spg
import (
"fmt"
"math"
"strings"
)
// WLRecipe (Word List password Attributes) are the generator settings for wordlist (syllable list) passwords
type WLRecipe struct {
list *WordList // Set of words for generating passwords
Length int // Length of generated password in words
SeparatorChar string // What character(s) should separate words
SeparatorFunc SFFunction // function to generate separators, If nil just use SeperatorChar
Capitalize CapScheme // Which words in generated password should be capitalized
}
// CapScheme is for an enumeration of capitalization schemes
type CapScheme string
// Defined capitalization schemes. (Using strings instead of int enum
// to make life easier in a debugger and calling from JavaScript)
const (
CSNone CapScheme = "none" // No words will be capitalized
CSFirst CapScheme = "first" // First word will be capitalized
CSAll CapScheme = "all" // All words will be capitalized
CSRandom CapScheme = "random" // Some words (roughly half) will be capitalized
CSOne CapScheme = "one" // One randomly selected word will be capitalized
)
// NewWLRecipe sets up word list password attributes with defaults and Length length
func NewWLRecipe(length int, wl *WordList) *WLRecipe {
attrs := &WLRecipe{
Length: length,
Capitalize: CSNone,
list: wl,
}
return attrs
}
// WordList contains the list of words WLGenerator()
type WordList struct {
words []string
unCapitalizableCount int
}
// Size of the wordlist in the recipe
func (r WLRecipe) Size() uint32 {
return r.list.Size()
}
// Size returns the number of items in the generator's wordlist or the maxiumum uint32, whichever is smaller
// (the restriction on size is because of the RNG we are using)
func (wl WordList) Size() uint32 {
size := len(wl.words)
// Why all this casting? (yes, functions not casts.) Because gopherjs won't assign
// math.MaxUint32 to an int. It doesn't like untyped values an considers it overflow
if uint64(size) > uint64(math.MaxUint32) {
return uint32(math.MaxUint32)
}
return uint32(size)
}
// NewWordList does what it says on the tin. Pass it a slice of strings
// It will remove duplicates from the slice provided, and it
// will count up how many words on the list can be changed through capitalization
// This isn't cheap, so it is best to create each word list once and keep it around
// as long as you need it.
func NewWordList(list []string) (*WordList, error) {
if len(list) == 0 {
return nil, fmt.Errorf("cannot set up word list generator without words")
}
// Our RNG for picking from a list returns a uint32, so that places an upper limit on size of list
if uint64(len(list)) > uint64(math.MaxUint32) {
return nil, fmt.Errorf("we can't handle more than %d words", uint32(0xFFFFFFFF))
}
// We want to ensure that no item appears more than once
unique := make(map[string]bool)
for _, word := range list {
if !unique[word] {
unique[word] = true
}
}
// A second pass to find out how many words have distinct capitalizations
// This also treats "Polish" and "polish" as duplicates, and will
// remove the Capitalized one from the list
//
// This pass also assumes that everything in unique is "true"
unCapable := 0
for w := range unique {
if unique[w] { // it may have been deleted since range was computed
cap := strings.Title(w)
if unique[cap] {
if cap != w { // w is "polish"
delete(unique, cap) // delete won't change what is in range
} else {
unCapable++
}
}
}
}
// third pass, because life sucks
var ourWords []string
for w := range unique {
ourWords = append(ourWords, w)
}
if len(list) > len(ourWords) {
// We just need to log a warning here. Not sure how we are handling that.
// I could create a brain with standard logger and use that, but that seems
// wrong. So let's just do this
fmt.Printf("%d duplicate words found when setting up word list generator\n", len(list)-len(ourWords))
}
result := &WordList{
words: ourWords,
unCapitalizableCount: unCapable,
}
return result, nil
}
// Generate a password using the wordlist recipe.
func (r WLRecipe) Generate() (*Password, error) {
p := &Password{}
if r.Size() == 0 {
return nil, fmt.Errorf("wordlist generator must be set up before being used")
}
if r.Length < 1 {
return nil, fmt.Errorf("don't ask for passwords of length %d", r.Length)
}
var sf SFFunction
if r.SeparatorFunc == nil {
sf = SFFunction(func() (string, FloatE) { return r.SeparatorChar, 0.0 })
} else {
sf = r.SeparatorFunc
}
// Construct a map of which words to capitalize
capWords := make(map[int]bool, r.Length)
switch r.Capitalize {
case CSFirst:
capWords[0] = true
case CSOne:
w := int(randomUint32n(uint32(r.Length)))
capWords[w] = true
case CSRandom:
for i := 0; i < r.Length; i++ {
if randomUint32n(2) == 1 {
capWords[i] = true
}
}
case CSAll:
for i := 0; i < r.Length; i++ {
capWords[i] = true
}
}
ts := []Token{}
for i := 0; i < r.Length; i++ {
w := r.list.words[randomUint32n(uint32(r.Size()))]
if capWords[i] {
w = strings.Title(w)
}
if len(w) > 0 {
ts = append(ts, Token{w, AtomType})
}
if i < r.Length-1 {
sep, _ := sf()
if len(sep) > 0 {
ts = append(ts, Token{sep, SeparatorType})
}
}
}
p.tokens = ts
p.Entropy = r.Entropy()
return p, nil
}
// Entropy returns the min-entropy from the recipe. It needs to know things
// about the wordlist used as well as other details of the recipe.
//
// When the generator produces uniform distirbution (the typical case) min-entropy
// and Shannon entropy are the same. If capitalization is used and the word list
// contains members whose capitalization does not yield a distinct element,
// the distribution becomes non-uniform.
func (r WLRecipe) Entropy() float32 {
size := int(r.Size())
ent := entropySimple(r.Length, size)
// Contribution of Capitalization scheme
if r.list.isAllCapitalizable() {
switch r.Capitalize {
case CSRandom:
ent += FloatE(float64(r.Length))
case CSOne:
ent += FloatE(math.Log2(float64(r.Length)))
default: // No change in entropy
}
}
// else there is no additional entropy contribution from capitalization
// Entropy contribution of separators
sepEnt := FloatE(0.0)
if r.SeparatorFunc != nil {
_, sepEnt = r.SeparatorFunc()
}
ent += (FloatE(r.Length) - 1.0) * sepEnt
return float32(ent)
}
func (wl *WordList) isAllCapitalizable() bool {
if wl.unCapitalizableCount > 0 {
return false
}
return true
}
func (wl *WordList) capitalizeRatio() float64 {
s := float64(len(wl.words))
return (s - float64(wl.unCapitalizableCount)) / s
}
/*** Separator functions
Wordlist (syllable list) type generators need separators between the words,
and creating and setting separator functions is useful. That is what is
defined in this section.
***/
// SFFunction is a type for a function that returns a string
// (to be used within a password) and the entropy it contributes
type SFFunction func() (string, FloatE)
// NewSFFunction makes a Separator Function from a CharRecipe
func NewSFFunction(r CharRecipe) SFFunction {
// I need to learn how to proper create factories.
var sf SFFunction
sf = func() (string, FloatE) { return sfWrap(r) }
return sf
}
// Pre-baked Separator functions
func sfWrap(r CharRecipe) (string, FloatE) {
p, err := r.Generate()
// perhaps not the best error handling, but can't think of anything better
if err != nil {
return "", 0.0
}
return p.String(), FloatE(p.Entropy)
}
// SFNone empty separator
// func SFNone() (string, FloatE) { return "", 0.0 }
// Pre-baked Separator functions
var (
SFNone SFFunction = func() (string, FloatE) { return "", FloatE(0.0) } // Empty separator
SFDigits1 = NewSFFunction(CharRecipe{Length: 1, Allow: Digits}) // Single digit separator
SFDigits2 = NewSFFunction(CharRecipe{Length: 2, Allow: Digits}) // Double digit separator
SFDigitsNoAmbiguous1 = NewSFFunction(CharRecipe{Length: 1, Allow: Digits, Exclude: Ambiguous}) // Single digit, no ambiguous
SFDigitsNoAmbiguous2 = NewSFFunction(CharRecipe{Length: 2, Allow: Digits, Exclude: Ambiguous}) // Double digit, no ambiguous
SFSymbols = NewSFFunction(CharRecipe{Length: 1, Allow: Symbols}) // Symbols
SFDigitsSymbols = NewSFFunction(CharRecipe{Length: 1, Allow: Symbols | Digits}) // Symbols and digits
)
/**
** Copyright 2018 AgileBits, Inc.
** Licensed under the Apache License, Version 2.0 (the "License").
**/