Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

muesli / crunchy / 94

17 Aug 2020 - 12:37 coverage: 90.612% (-2.9%) from 93.562%
94

Pull #9

travis-ci

Moran Golan
update readme file
Pull Request #9: Add MustContainDigit and MustContainSymbol flags on the Option struct

7 of 15 new or added lines in 1 file covered. (46.67%)

222 of 245 relevant lines covered (90.61%)

54368.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.81
/crunchy.go
1
/*
2
 * crunchy - find common flaws in passwords
3
 *     Copyright (c) 2017-2018, Christian Muehlhaeuser <muesli@gmail.com>
4
 *
5
 *   For license see LICENSE
6
 */
7

8
package crunchy
9

10
import (
11
        "bufio"
12
        "encoding/hex"
13
        "hash"
14
        "os"
15
        "path/filepath"
16
        "regexp"
17
        "strings"
18
        "sync"
19
        "unicode"
20
        "unicode/utf8"
21

22
        "github.com/xrash/smetrics"
23
)
24

25
// Validator is used to setup a new password validator with options and dictionaries
26
type Validator struct {
27
        options     Options
28
        once        sync.Once
29
        wordsMaxLen int                 // length of longest word in dictionaries
30
        words       map[string]struct{} // map to index parsed dictionaries
31
        hashedWords map[string]string   // maps hash-sum to password
32
}
33

34
// Options contains all the settings for a Validator
35
type Options struct {
36
        // MinLength is the minimum length required for a valid password (>=1, default is 8)
37
        MinLength int
38
        // MinDiff is the minimum amount of unique characters required for a valid password (>=1, default is 5)
39
        MinDiff int
40
        // MinDist is the minimum WagnerFischer distance for mangled password dictionary lookups (>=0, default is 3)
41
        MinDist int
42
        // Hashers will be used to find hashed passwords in dictionaries
43
        Hashers []hash.Hash
44
        // DictionaryPath contains all the dictionaries that will be parsed (default is /usr/share/dict)
45
        DictionaryPath string
46
        // Check haveibeenpwned.com database
47
        CheckHIBP bool
48
        // MustContainDigit requires at least one digit for a valid password
49
        MustContainDigit bool
50
        // MustContainSymbol requires at least one special symbol for a valid password
51
        MustContainSymbol bool
52
}
53

54
// NewValidator returns a new password validator with default settings
55
func NewValidator() *Validator {
1×
56
        return NewValidatorWithOpts(Options{
1×
57
                MinDist:           -1,
1×
58
                DictionaryPath:    "/usr/share/dict",
1×
59
                CheckHIBP:         false,
1×
60
                MustContainDigit:  false,
1×
61
                MustContainSymbol: false,
1×
62
        })
1×
63
}
1×
64

65
// NewValidatorWithOpts returns a new password validator with custom settings
66
func NewValidatorWithOpts(options Options) *Validator {
3×
67
        if options.MinLength <= 0 {
6×
68
                options.MinLength = 8
3×
69
        }
3×
70
        if options.MinDiff <= 0 {
6×
71
                options.MinDiff = 5
3×
72
        }
3×
73
        if options.MinDist < 0 {
5×
74
                options.MinDist = 3
2×
75
        }
2×
76

77
        return &Validator{
3×
78
                options:     options,
3×
79
                words:       make(map[string]struct{}),
3×
80
                hashedWords: make(map[string]string),
3×
81
        }
3×
82
}
83

84
// indexDictionaries parses dictionaries/wordlists
85
func (v *Validator) indexDictionaries() {
2×
86
        if v.options.DictionaryPath == "" {
3×
87
                return
1×
88
        }
1×
89

90
        dicts, err := filepath.Glob(filepath.Join(v.options.DictionaryPath, "*"))
1×
91
        if err != nil {
1×
92
                return
!
93
        }
!
94

95
        for _, dict := range dicts {
4×
96
                file, err := os.Open(dict)
3×
97
                if err != nil {
3×
98
                        continue
!
99
                }
100
                defer file.Close()
3×
101

3×
102
                scanner := bufio.NewScanner(file)
3×
103
                for scanner.Scan() {
251,220×
104
                        nw := normalize(scanner.Text())
251,217×
105
                        nwlen := len(nw)
251,217×
106
                        if nwlen > v.wordsMaxLen {
251,234×
107
                                v.wordsMaxLen = nwlen
17×
108
                        }
17×
109

110
                        // if a word is smaller than the minimum length minus the minimum distance
111
                        // then any collisons would have been rejected by pre-dictionary checks
112
                        if nwlen >= v.options.MinLength-v.options.MinDist {
490,076×
113
                                v.words[nw] = struct{}{}
238,859×
114
                        }
238,859×
115

116
                        for _, hasher := range v.options.Hashers {
1,256,085×
117
                                v.hashedWords[hashsum(nw, hasher)] = nw
1,004,868×
118
                        }
1,004,868×
119
                }
120
        }
121
}
122

123
// foundInDictionaries returns whether a (mangled) string exists in the indexed dictionaries
124
func (v *Validator) foundInDictionaries(s string) error {
18×
125
        v.once.Do(v.indexDictionaries)
18×
126

18×
127
        pw := normalize(s)   // normalized password
18×
128
        revpw := reverse(pw) // reversed password
18×
129
        pwlen := len(pw)
18×
130

18×
131
        // let's check perfect matches first
18×
132
        // we can skip this if the pw is longer than the longest word in our dictionary
18×
133
        if pwlen <= v.wordsMaxLen {
30×
134
                if _, ok := v.words[pw]; ok {
14×
135
                        return &DictionaryError{ErrDictionary, pw, 0}
2×
136
                }
2×
137
                if _, ok := v.words[revpw]; ok {
11×
138
                        return &DictionaryError{ErrMangledDictionary, revpw, 0}
1×
139
                }
1×
140
        }
141

142
        // find hashed dictionary entries
143
        if pwindex, err := hex.DecodeString(pw); err == nil {
21×
144
                if word, ok := v.hashedWords[string(pwindex)]; ok {
10×
145
                        return &HashedDictionaryError{ErrHashedDictionary, word}
4×
146
                }
4×
147
        }
148

149
        // find mangled / reversed passwords
150
        // we can skip this if the pw is longer than the longest word plus our minimum distance
151
        if pwlen <= v.wordsMaxLen+v.options.MinDist {
20×
152
                for word := range v.words {
764,729×
153
                        if dist := smetrics.WagnerFischer(word, pw, 1, 1, 1); dist <= v.options.MinDist {
764,722×
154
                                return &DictionaryError{ErrMangledDictionary, word, dist}
2×
155
                        }
2×
156
                        if dist := smetrics.WagnerFischer(word, revpw, 1, 1, 1); dist <= v.options.MinDist {
764,719×
157
                                return &DictionaryError{ErrMangledDictionary, word, dist}
1×
158
                        }
1×
159
                }
160
        }
161

162
        return nil
8×
163
}
164

165
// Check validates a password for common flaws
166
// It returns nil if the password is considered acceptable.
167
func (v *Validator) Check(password string) error {
29×
168
        if strings.TrimSpace(password) == "" {
31×
169
                return ErrEmpty
2×
170
        }
2×
171
        if len(password) < v.options.MinLength {
29×
172
                return ErrTooShort
2×
173
        }
2×
174
        if countUniqueChars(password) < v.options.MinDiff {
28×
175
                return ErrTooFewChars
3×
176
        }
3×
177

178
        // Inspired by cracklib
179
        maxrepeat := 3.0 + (0.09 * float64(len(password)))
22×
180
        if countSystematicChars(password) > int(maxrepeat) {
26×
181
                return ErrTooSystematic
4×
182
        }
4×
183

184
        err := v.foundInDictionaries(password)
18×
185
        if err != nil {
28×
186
                return err
10×
187
        }
10×
188

189
        if v.options.CheckHIBP {
9×
190
                err := foundInHIBP(password)
1×
191
                if err != nil {
2×
192
                        return err
1×
193
                }
1×
194
        }
195

196
        if v.options.MustContainDigit {
7×
NEW
197
                validateDigit := regexp.MustCompile(`[0-9]+`)
!
NEW
198
                if !validateDigit.MatchString(password) {
!
NEW
199
                        return ErrNoDigits
!
NEW
200
                }
!
201
        }
202

203
        if v.options.MustContainSymbol {
7×
NEW
204
                validateSymbols := regexp.MustCompile(`[^\w\s]+`)
!
NEW
205
                if !validateSymbols.MatchString(password) {
!
NEW
206
                        return ErrNoSymbols
!
NEW
207
                }
!
208
        }
209

210
        return nil
7×
211
}
212

213
// Rate grades a password's strength from 0 (weak) to 100 (strong).
214
func (v *Validator) Rate(password string) (uint, error) {
27×
215
        if err := v.Check(password); err != nil {
47×
216
                return 0, err
20×
217
        }
20×
218

219
        l := len(password)
7×
220
        systematics := countSystematicChars(password)
7×
221
        repeats := l - countUniqueChars(password)
7×
222
        var letters, uLetters, numbers, symbols int
7×
223

7×
224
        for len(password) > 0 {
103×
225
                r, size := utf8.DecodeRuneInString(password)
96×
226
                password = password[size:]
96×
227

96×
228
                if unicode.IsLetter(r) {
145×
229
                        if unicode.IsUpper(r) {
60×
230
                                uLetters++
11×
231
                        } else {
49×
232
                                letters++
38×
233
                        }
38×
234
                } else if unicode.IsNumber(r) {
84×
235
                        numbers++
37×
236
                } else {
47×
237
                        symbols++
10×
238
                }
10×
239
        }
240

241
        // ADD: number of characters
242
        n := l * 4
7×
243
        // ADD: uppercase letters
7×
244
        if uLetters > 0 {
12×
245
                n += (l - uLetters) * 2
5×
246
        }
5×
247
        // ADD: lowercase letters
248
        if letters > 0 {
13×
249
                n += (l - letters) * 2
6×
250
        }
6×
251
        // ADD: numbers
252
        n += numbers * 4
7×
253
        // ADD: symbols
7×
254
        n += symbols * 6
7×
255

7×
256
        // REM: letters only
7×
257
        if l == letters+uLetters {
8×
258
                n -= letters + uLetters
1×
259
        }
1×
260
        // REM: numbers only
261
        if l == numbers {
8×
262
                n -= numbers * 4
1×
263
        }
1×
264
        // REM: repeat characters (case insensitive)
265
        n -= repeats * 4
7×
266
        // REM: systematic characters
7×
267
        n -= systematics * 3
7×
268

7×
269
        if n < 0 {
7×
270
                n = 0
!
271
        } else if n > 100 {
9×
272
                n = 100
2×
273
        }
2×
274
        return uint(n), nil
7×
275
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc