zxcvbn-ruby 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +23 -0
- data/Rakefile +18 -0
- data/data/adjacency_graphs.json +9 -0
- data/data/frequency_lists.yaml +85094 -0
- data/lib/zxcvbn.rb +37 -0
- data/lib/zxcvbn/crack_time.rb +51 -0
- data/lib/zxcvbn/dictionary_ranker.rb +23 -0
- data/lib/zxcvbn/entropy.rb +151 -0
- data/lib/zxcvbn/match.rb +13 -0
- data/lib/zxcvbn/matchers/date.rb +134 -0
- data/lib/zxcvbn/matchers/dictionary.rb +34 -0
- data/lib/zxcvbn/matchers/digits.rb +18 -0
- data/lib/zxcvbn/matchers/l33t.rb +127 -0
- data/lib/zxcvbn/matchers/new_l33t.rb +120 -0
- data/lib/zxcvbn/matchers/regex_helpers.rb +21 -0
- data/lib/zxcvbn/matchers/repeat.rb +32 -0
- data/lib/zxcvbn/matchers/sequences.rb +64 -0
- data/lib/zxcvbn/matchers/spatial.rb +79 -0
- data/lib/zxcvbn/matchers/year.rb +18 -0
- data/lib/zxcvbn/math.rb +63 -0
- data/lib/zxcvbn/omnimatch.rb +49 -0
- data/lib/zxcvbn/password_strength.rb +21 -0
- data/lib/zxcvbn/score.rb +15 -0
- data/lib/zxcvbn/scorer.rb +84 -0
- data/lib/zxcvbn/version.rb +3 -0
- data/spec/matchers/date_spec.rb +109 -0
- data/spec/matchers/dictionary_spec.rb +14 -0
- data/spec/matchers/digits_spec.rb +15 -0
- data/spec/matchers/l33t_spec.rb +85 -0
- data/spec/matchers/repeat_spec.rb +18 -0
- data/spec/matchers/sequences_spec.rb +16 -0
- data/spec/matchers/spatial_spec.rb +20 -0
- data/spec/matchers/year_spec.rb +15 -0
- data/spec/omnimatch_spec.rb +24 -0
- data/spec/scorer_spec.rb +5 -0
- data/spec/scoring/crack_time_spec.rb +106 -0
- data/spec/scoring/entropy_spec.rb +213 -0
- data/spec/scoring/math_spec.rb +131 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/support/js_helpers.rb +35 -0
- data/spec/support/js_source/adjacency_graphs.js +8 -0
- data/spec/support/js_source/compiled.js +1188 -0
- data/spec/support/js_source/frequency_lists.js +10 -0
- data/spec/support/js_source/init.coffee +63 -0
- data/spec/support/js_source/init.js +95 -0
- data/spec/support/js_source/matching.coffee +444 -0
- data/spec/support/js_source/matching.js +685 -0
- data/spec/support/js_source/scoring.coffee +270 -0
- data/spec/support/js_source/scoring.js +390 -0
- data/spec/support/matcher.rb +35 -0
- data/spec/zxcvbn_spec.rb +49 -0
- data/zxcvbn-ruby.gemspec +20 -0
- metadata +167 -0
@@ -0,0 +1,270 @@
|
|
1
|
+
|
2
|
+
nCk = (n, k) ->
|
3
|
+
# http://blog.plover.com/math/choose.html
|
4
|
+
return 0 if k > n
|
5
|
+
return 1 if k == 0
|
6
|
+
r = 1
|
7
|
+
for d in [1..k]
|
8
|
+
r *= n
|
9
|
+
r /= d
|
10
|
+
n -= 1
|
11
|
+
r
|
12
|
+
|
13
|
+
lg = (n) -> Math.log(n) / Math.log(2)
|
14
|
+
|
15
|
+
# ------------------------------------------------------------------------------
|
16
|
+
# minimum entropy search -------------------------------------------------------
|
17
|
+
# ------------------------------------------------------------------------------
|
18
|
+
#
|
19
|
+
# takes a list of overlapping matches, returns the non-overlapping sublist with
|
20
|
+
# minimum entropy. O(nm) dp alg for length-n password with m candidate matches.
|
21
|
+
# ------------------------------------------------------------------------------
|
22
|
+
|
23
|
+
minimum_entropy_match_sequence = (password, matches) ->
|
24
|
+
bruteforce_cardinality = calc_bruteforce_cardinality password # e.g. 26 for lowercase
|
25
|
+
up_to_k = [] # minimum entropy up to k.
|
26
|
+
backpointers = [] # for the optimal sequence of matches up to k, holds the final match (match.j == k). null means the sequence ends w/ a brute-force character.
|
27
|
+
for k in [0...password.length]
|
28
|
+
# starting scenario to try and beat: adding a brute-force character to the minimum entropy sequence at k-1.
|
29
|
+
up_to_k[k] = (up_to_k[k-1] or 0) + lg bruteforce_cardinality
|
30
|
+
backpointers[k] = null
|
31
|
+
for match in matches when match.j == k
|
32
|
+
[i, j] = [match.i, match.j]
|
33
|
+
# see if best entropy up to i-1 + entropy of this match is less than the current minimum at j.
|
34
|
+
candidate_entropy = (up_to_k[i-1] or 0) + calc_entropy(match)
|
35
|
+
if candidate_entropy < up_to_k[j]
|
36
|
+
up_to_k[j] = candidate_entropy
|
37
|
+
backpointers[j] = match
|
38
|
+
|
39
|
+
# walk backwards and decode the best sequence
|
40
|
+
match_sequence = []
|
41
|
+
k = password.length - 1
|
42
|
+
while k >= 0
|
43
|
+
match = backpointers[k]
|
44
|
+
if match
|
45
|
+
match_sequence.push match
|
46
|
+
k = match.i - 1
|
47
|
+
else
|
48
|
+
k -= 1
|
49
|
+
match_sequence.reverse()
|
50
|
+
|
51
|
+
# fill in the blanks between pattern matches with bruteforce "matches"
|
52
|
+
# that way the match sequence fully covers the password: match1.j == match2.i - 1 for every adjacent match1, match2.
|
53
|
+
make_bruteforce_match = (i, j) ->
|
54
|
+
pattern: 'bruteforce'
|
55
|
+
i: i
|
56
|
+
j: j
|
57
|
+
token: password[i..j]
|
58
|
+
entropy: lg Math.pow(bruteforce_cardinality, j - i + 1)
|
59
|
+
cardinality: bruteforce_cardinality
|
60
|
+
k = 0
|
61
|
+
match_sequence_copy = []
|
62
|
+
for match in match_sequence
|
63
|
+
[i, j] = [match.i, match.j]
|
64
|
+
if i - k > 0
|
65
|
+
match_sequence_copy.push make_bruteforce_match(k, i - 1)
|
66
|
+
k = j + 1
|
67
|
+
match_sequence_copy.push match
|
68
|
+
if k < password.length
|
69
|
+
match_sequence_copy.push make_bruteforce_match(k, password.length - 1)
|
70
|
+
match_sequence = match_sequence_copy
|
71
|
+
|
72
|
+
min_entropy = up_to_k[password.length - 1] or 0 # or 0 corner case is for an empty password ''
|
73
|
+
crack_time = entropy_to_crack_time min_entropy
|
74
|
+
|
75
|
+
# final result object
|
76
|
+
password: password
|
77
|
+
entropy: round_to_x_digits(min_entropy, 3)
|
78
|
+
match_sequence: match_sequence
|
79
|
+
crack_time: round_to_x_digits(crack_time, 3)
|
80
|
+
crack_time_display: display_time crack_time
|
81
|
+
score: crack_time_to_score crack_time
|
82
|
+
|
83
|
+
round_to_x_digits = (n, x) -> Math.round(n * Math.pow(10, x)) / Math.pow(10, x)
|
84
|
+
|
85
|
+
# ------------------------------------------------------------------------------
|
86
|
+
# threat model -- stolen hash catastrophe scenario -----------------------------
|
87
|
+
# ------------------------------------------------------------------------------
|
88
|
+
#
|
89
|
+
# assumes:
|
90
|
+
# * passwords are stored as salted hashes, different random salt per user.
|
91
|
+
# (making rainbow attacks infeasable.)
|
92
|
+
# * hashes and salts were stolen. attacker is guessing passwords at max rate.
|
93
|
+
# * attacker has several CPUs at their disposal.
|
94
|
+
# ------------------------------------------------------------------------------
|
95
|
+
|
96
|
+
# for a hash function like bcrypt/scrypt/PBKDF2, 10ms per guess is a safe lower bound.
|
97
|
+
# (usually a guess would take longer -- this assumes fast hardware and a small work factor.)
|
98
|
+
# adjust for your site accordingly if you use another hash function, possibly by
|
99
|
+
# several orders of magnitude!
|
100
|
+
SINGLE_GUESS = .010
|
101
|
+
NUM_ATTACKERS = 100 # number of cores guessing in parallel.
|
102
|
+
|
103
|
+
SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS
|
104
|
+
|
105
|
+
entropy_to_crack_time = (entropy) -> .5 * Math.pow(2, entropy) * SECONDS_PER_GUESS # average, not total
|
106
|
+
|
107
|
+
crack_time_to_score = (seconds) ->
|
108
|
+
return 0 if seconds < Math.pow(10, 2)
|
109
|
+
return 1 if seconds < Math.pow(10, 4)
|
110
|
+
return 2 if seconds < Math.pow(10, 6)
|
111
|
+
return 3 if seconds < Math.pow(10, 8)
|
112
|
+
return 4
|
113
|
+
|
114
|
+
# ------------------------------------------------------------------------------
|
115
|
+
# entropy calcs -- one function per match pattern ------------------------------
|
116
|
+
# ------------------------------------------------------------------------------
|
117
|
+
|
118
|
+
calc_entropy = (match) ->
|
119
|
+
return match.entropy if match.entropy? # a match's entropy doesn't change. cache it.
|
120
|
+
entropy_func = switch match.pattern
|
121
|
+
when 'repeat' then repeat_entropy
|
122
|
+
when 'sequence' then sequence_entropy
|
123
|
+
when 'digits' then digits_entropy
|
124
|
+
when 'year' then year_entropy
|
125
|
+
when 'date' then date_entropy
|
126
|
+
when 'spatial' then spatial_entropy
|
127
|
+
when 'dictionary' then dictionary_entropy
|
128
|
+
match.entropy = entropy_func match
|
129
|
+
|
130
|
+
repeat_entropy = (match) ->
|
131
|
+
cardinality = calc_bruteforce_cardinality match.token
|
132
|
+
lg (cardinality * match.token.length)
|
133
|
+
|
134
|
+
sequence_entropy = (match) ->
|
135
|
+
first_chr = match.token.charAt(0)
|
136
|
+
if first_chr in ['a', '1']
|
137
|
+
base_entropy = 1
|
138
|
+
else
|
139
|
+
if first_chr.match /\d/
|
140
|
+
base_entropy = lg(10) # digits
|
141
|
+
else if first_chr.match /[a-z]/
|
142
|
+
base_entropy = lg(26) # lower
|
143
|
+
else
|
144
|
+
base_entropy = lg(26) + 1 # extra bit for uppercase
|
145
|
+
if not match.ascending
|
146
|
+
base_entropy += 1 # extra bit for descending instead of ascending
|
147
|
+
base_entropy + lg match.token.length
|
148
|
+
|
149
|
+
digits_entropy = (match) -> lg Math.pow(10, match.token.length)
|
150
|
+
|
151
|
+
NUM_YEARS = 119 # years match against 1900 - 2019
|
152
|
+
NUM_MONTHS = 12
|
153
|
+
NUM_DAYS = 31
|
154
|
+
|
155
|
+
year_entropy = (match) -> lg NUM_YEARS
|
156
|
+
|
157
|
+
date_entropy = (match) ->
|
158
|
+
if match.year < 100
|
159
|
+
entropy = lg(NUM_DAYS * NUM_MONTHS * 100) # two-digit year
|
160
|
+
else
|
161
|
+
entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS) # four-digit year
|
162
|
+
if match.separator
|
163
|
+
entropy += 2 # add two bits for separator selection [/,-,.,etc]
|
164
|
+
entropy
|
165
|
+
|
166
|
+
spatial_entropy = (match) ->
|
167
|
+
if match.graph in ['qwerty', 'dvorak']
|
168
|
+
s = KEYBOARD_STARTING_POSITIONS
|
169
|
+
d = KEYBOARD_AVERAGE_DEGREE
|
170
|
+
else
|
171
|
+
s = KEYPAD_STARTING_POSITIONS
|
172
|
+
d = KEYPAD_AVERAGE_DEGREE
|
173
|
+
possibilities = 0
|
174
|
+
L = match.token.length
|
175
|
+
t = match.turns
|
176
|
+
# estimate the number of possible patterns w/ length L or less with t turns or less.
|
177
|
+
for i in [2..L]
|
178
|
+
possible_turns = Math.min(t, i - 1)
|
179
|
+
for j in [1..possible_turns]
|
180
|
+
possibilities += nCk(i - 1, j - 1) * s * Math.pow(d, j)
|
181
|
+
entropy = lg possibilities
|
182
|
+
# add extra entropy for shifted keys. (% instead of 5, A instead of a.)
|
183
|
+
# math is similar to extra entropy from uppercase letters in dictionary matches.
|
184
|
+
if match.shifted_count
|
185
|
+
S = match.shifted_count
|
186
|
+
U = match.token.length - match.shifted_count # unshifted count
|
187
|
+
possibilities = 0
|
188
|
+
possibilities += nCk(S + U, i) for i in [0..Math.min(S, U)]
|
189
|
+
entropy += lg possibilities
|
190
|
+
entropy
|
191
|
+
|
192
|
+
dictionary_entropy = (match) ->
|
193
|
+
match.base_entropy = lg match.rank # keep these as properties for display purposes
|
194
|
+
match.uppercase_entropy = extra_uppercase_entropy match
|
195
|
+
match.l33t_entropy = extra_l33t_entropy match
|
196
|
+
match.base_entropy + match.uppercase_entropy + match.l33t_entropy
|
197
|
+
|
198
|
+
START_UPPER = /^[A-Z][^A-Z]+$/
|
199
|
+
END_UPPER = /^[^A-Z]+[A-Z]$/
|
200
|
+
ALL_UPPER = /^[^a-z]+$/
|
201
|
+
ALL_LOWER = /^[^A-Z]+$/
|
202
|
+
|
203
|
+
extra_uppercase_entropy = (match) ->
|
204
|
+
word = match.token
|
205
|
+
return 0 if word.match ALL_LOWER
|
206
|
+
# a capitalized word is the most common capitalization scheme,
|
207
|
+
# so it only doubles the search space (uncapitalized + capitalized): 1 extra bit of entropy.
|
208
|
+
# allcaps and end-capitalized are common enough too, underestimate as 1 extra bit to be safe.
|
209
|
+
for regex in [START_UPPER, END_UPPER, ALL_UPPER]
|
210
|
+
return 1 if word.match regex
|
211
|
+
# otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters with U uppercase letters or less.
|
212
|
+
# or, if there's more uppercase than lower (for e.g. PASSwORD), the number of ways to lowercase U+L letters with L lowercase letters or less.
|
213
|
+
U = (chr for chr in word.split('') when chr.match /[A-Z]/).length
|
214
|
+
L = (chr for chr in word.split('') when chr.match /[a-z]/).length
|
215
|
+
possibilities = 0
|
216
|
+
possibilities += nCk(U + L, i) for i in [0..Math.min(U, L)]
|
217
|
+
lg possibilities
|
218
|
+
|
219
|
+
extra_l33t_entropy = (match) ->
|
220
|
+
return 0 if not match.l33t
|
221
|
+
possibilities = 0
|
222
|
+
for subbed, unsubbed of match.sub
|
223
|
+
S = (chr for chr in match.token.split('') when chr == subbed).length # number of subbed characters.
|
224
|
+
U = (chr for chr in match.token.split('') when chr == unsubbed).length # number of unsubbed characters.
|
225
|
+
possibilities += nCk(U + S, i) for i in [0..Math.min(U, S)]
|
226
|
+
# corner: return 1 bit for single-letter subs, like 4pple -> apple, instead of 0.
|
227
|
+
lg(possibilities) or 1
|
228
|
+
|
229
|
+
# utilities --------------------------------------------------------------------
|
230
|
+
|
231
|
+
calc_bruteforce_cardinality = (password) ->
|
232
|
+
[lower, upper, digits, symbols] = [false, false, false, false]
|
233
|
+
for chr in password.split('')
|
234
|
+
ord = chr.charCodeAt(0)
|
235
|
+
if 0x30 <= ord <= 0x39
|
236
|
+
digits = true
|
237
|
+
else if 0x41 <= ord <= 0x5a
|
238
|
+
upper = true
|
239
|
+
else if 0x61 <= ord <= 0x7a
|
240
|
+
lower = true
|
241
|
+
else
|
242
|
+
symbols = true
|
243
|
+
c = 0
|
244
|
+
c += 10 if digits
|
245
|
+
c += 26 if upper
|
246
|
+
c += 26 if lower
|
247
|
+
c += 33 if symbols
|
248
|
+
c
|
249
|
+
|
250
|
+
display_time = (seconds) ->
|
251
|
+
minute = 60
|
252
|
+
hour = minute * 60
|
253
|
+
day = hour * 24
|
254
|
+
month = day * 31
|
255
|
+
year = month * 12
|
256
|
+
century = year * 100
|
257
|
+
if seconds < minute
|
258
|
+
'instant'
|
259
|
+
else if seconds < hour
|
260
|
+
"#{1 + Math.ceil(seconds / minute)} minutes"
|
261
|
+
else if seconds < day
|
262
|
+
"#{1 + Math.ceil(seconds / hour)} hours"
|
263
|
+
else if seconds < month
|
264
|
+
"#{1 + Math.ceil(seconds / day)} days"
|
265
|
+
else if seconds < year
|
266
|
+
"#{1 + Math.ceil(seconds / month)} months"
|
267
|
+
else if seconds < century
|
268
|
+
"#{1 + Math.ceil(seconds / year)} years"
|
269
|
+
else
|
270
|
+
'centuries'
|
@@ -0,0 +1,390 @@
|
|
1
|
+
// Generated by CoffeeScript 1.3.3
|
2
|
+
var ALL_LOWER, ALL_UPPER, END_UPPER, NUM_ATTACKERS, NUM_DAYS, NUM_MONTHS, NUM_YEARS, SECONDS_PER_GUESS, SINGLE_GUESS, START_UPPER, calc_bruteforce_cardinality, calc_entropy, crack_time_to_score, date_entropy, dictionary_entropy, digits_entropy, display_time, entropy_to_crack_time, extra_l33t_entropy, extra_uppercase_entropy, lg, minimum_entropy_match_sequence, nCk, repeat_entropy, round_to_x_digits, sequence_entropy, spatial_entropy, year_entropy;
|
3
|
+
|
4
|
+
nCk = function(n, k) {
|
5
|
+
var d, r, _i;
|
6
|
+
if (k > n) {
|
7
|
+
return 0;
|
8
|
+
}
|
9
|
+
if (k === 0) {
|
10
|
+
return 1;
|
11
|
+
}
|
12
|
+
r = 1;
|
13
|
+
for (d = _i = 1; 1 <= k ? _i <= k : _i >= k; d = 1 <= k ? ++_i : --_i) {
|
14
|
+
r *= n;
|
15
|
+
r /= d;
|
16
|
+
n -= 1;
|
17
|
+
}
|
18
|
+
return r;
|
19
|
+
};
|
20
|
+
|
21
|
+
lg = function(n) {
|
22
|
+
return Math.log(n) / Math.log(2);
|
23
|
+
};
|
24
|
+
|
25
|
+
minimum_entropy_match_sequence = function(password, matches) {
|
26
|
+
var backpointers, bruteforce_cardinality, candidate_entropy, crack_time, i, j, k, make_bruteforce_match, match, match_sequence, match_sequence_copy, min_entropy, up_to_k, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2;
|
27
|
+
bruteforce_cardinality = calc_bruteforce_cardinality(password);
|
28
|
+
up_to_k = [];
|
29
|
+
backpointers = [];
|
30
|
+
for (k = _i = 0, _ref = password.length; 0 <= _ref ? _i < _ref : _i > _ref; k = 0 <= _ref ? ++_i : --_i) {
|
31
|
+
up_to_k[k] = (up_to_k[k - 1] || 0) + lg(bruteforce_cardinality);
|
32
|
+
backpointers[k] = null;
|
33
|
+
for (_j = 0, _len = matches.length; _j < _len; _j++) {
|
34
|
+
match = matches[_j];
|
35
|
+
if (!(match.j === k)) {
|
36
|
+
continue;
|
37
|
+
}
|
38
|
+
_ref1 = [match.i, match.j], i = _ref1[0], j = _ref1[1];
|
39
|
+
candidate_entropy = (up_to_k[i - 1] || 0) + calc_entropy(match);
|
40
|
+
if (candidate_entropy < up_to_k[j]) {
|
41
|
+
up_to_k[j] = candidate_entropy;
|
42
|
+
backpointers[j] = match;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
46
|
+
match_sequence = [];
|
47
|
+
k = password.length - 1;
|
48
|
+
while (k >= 0) {
|
49
|
+
match = backpointers[k];
|
50
|
+
if (match) {
|
51
|
+
match_sequence.push(match);
|
52
|
+
k = match.i - 1;
|
53
|
+
} else {
|
54
|
+
k -= 1;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
match_sequence.reverse();
|
58
|
+
make_bruteforce_match = function(i, j) {
|
59
|
+
return {
|
60
|
+
pattern: 'bruteforce',
|
61
|
+
i: i,
|
62
|
+
j: j,
|
63
|
+
token: password.slice(i, j + 1 || 9e9),
|
64
|
+
entropy: lg(Math.pow(bruteforce_cardinality, j - i + 1)),
|
65
|
+
cardinality: bruteforce_cardinality
|
66
|
+
};
|
67
|
+
};
|
68
|
+
k = 0;
|
69
|
+
match_sequence_copy = [];
|
70
|
+
for (_k = 0, _len1 = match_sequence.length; _k < _len1; _k++) {
|
71
|
+
match = match_sequence[_k];
|
72
|
+
_ref2 = [match.i, match.j], i = _ref2[0], j = _ref2[1];
|
73
|
+
if (i - k > 0) {
|
74
|
+
match_sequence_copy.push(make_bruteforce_match(k, i - 1));
|
75
|
+
}
|
76
|
+
k = j + 1;
|
77
|
+
match_sequence_copy.push(match);
|
78
|
+
}
|
79
|
+
if (k < password.length) {
|
80
|
+
match_sequence_copy.push(make_bruteforce_match(k, password.length - 1));
|
81
|
+
}
|
82
|
+
match_sequence = match_sequence_copy;
|
83
|
+
min_entropy = up_to_k[password.length - 1] || 0;
|
84
|
+
crack_time = entropy_to_crack_time(min_entropy);
|
85
|
+
return {
|
86
|
+
password: password,
|
87
|
+
entropy: round_to_x_digits(min_entropy, 3),
|
88
|
+
match_sequence: match_sequence,
|
89
|
+
crack_time: round_to_x_digits(crack_time, 3),
|
90
|
+
crack_time_display: display_time(crack_time),
|
91
|
+
score: crack_time_to_score(crack_time)
|
92
|
+
};
|
93
|
+
};
|
94
|
+
|
95
|
+
round_to_x_digits = function(n, x) {
|
96
|
+
return Math.round(n * Math.pow(10, x)) / Math.pow(10, x);
|
97
|
+
};
|
98
|
+
|
99
|
+
SINGLE_GUESS = .010;
|
100
|
+
|
101
|
+
NUM_ATTACKERS = 100;
|
102
|
+
|
103
|
+
SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS;
|
104
|
+
|
105
|
+
entropy_to_crack_time = function(entropy) {
|
106
|
+
return .5 * Math.pow(2, entropy) * SECONDS_PER_GUESS;
|
107
|
+
};
|
108
|
+
|
109
|
+
crack_time_to_score = function(seconds) {
|
110
|
+
if (seconds < Math.pow(10, 2)) {
|
111
|
+
return 0;
|
112
|
+
}
|
113
|
+
if (seconds < Math.pow(10, 4)) {
|
114
|
+
return 1;
|
115
|
+
}
|
116
|
+
if (seconds < Math.pow(10, 6)) {
|
117
|
+
return 2;
|
118
|
+
}
|
119
|
+
if (seconds < Math.pow(10, 8)) {
|
120
|
+
return 3;
|
121
|
+
}
|
122
|
+
return 4;
|
123
|
+
};
|
124
|
+
|
125
|
+
calc_entropy = function(match) {
|
126
|
+
var entropy_func;
|
127
|
+
if (match.entropy != null) {
|
128
|
+
return match.entropy;
|
129
|
+
}
|
130
|
+
entropy_func = (function() {
|
131
|
+
switch (match.pattern) {
|
132
|
+
case 'repeat':
|
133
|
+
return repeat_entropy;
|
134
|
+
case 'sequence':
|
135
|
+
return sequence_entropy;
|
136
|
+
case 'digits':
|
137
|
+
return digits_entropy;
|
138
|
+
case 'year':
|
139
|
+
return year_entropy;
|
140
|
+
case 'date':
|
141
|
+
return date_entropy;
|
142
|
+
case 'spatial':
|
143
|
+
return spatial_entropy;
|
144
|
+
case 'dictionary':
|
145
|
+
return dictionary_entropy;
|
146
|
+
}
|
147
|
+
})();
|
148
|
+
return match.entropy = entropy_func(match);
|
149
|
+
};
|
150
|
+
|
151
|
+
repeat_entropy = function(match) {
|
152
|
+
var cardinality;
|
153
|
+
cardinality = calc_bruteforce_cardinality(match.token);
|
154
|
+
return lg(cardinality * match.token.length);
|
155
|
+
};
|
156
|
+
|
157
|
+
sequence_entropy = function(match) {
|
158
|
+
var base_entropy, first_chr;
|
159
|
+
first_chr = match.token.charAt(0);
|
160
|
+
if (first_chr === 'a' || first_chr === '1') {
|
161
|
+
base_entropy = 1;
|
162
|
+
} else {
|
163
|
+
if (first_chr.match(/\d/)) {
|
164
|
+
base_entropy = lg(10);
|
165
|
+
} else if (first_chr.match(/[a-z]/)) {
|
166
|
+
base_entropy = lg(26);
|
167
|
+
} else {
|
168
|
+
base_entropy = lg(26) + 1;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
if (!match.ascending) {
|
172
|
+
base_entropy += 1;
|
173
|
+
}
|
174
|
+
return base_entropy + lg(match.token.length);
|
175
|
+
};
|
176
|
+
|
177
|
+
digits_entropy = function(match) {
|
178
|
+
return lg(Math.pow(10, match.token.length));
|
179
|
+
};
|
180
|
+
|
181
|
+
NUM_YEARS = 119;
|
182
|
+
|
183
|
+
NUM_MONTHS = 12;
|
184
|
+
|
185
|
+
NUM_DAYS = 31;
|
186
|
+
|
187
|
+
year_entropy = function(match) {
|
188
|
+
return lg(NUM_YEARS);
|
189
|
+
};
|
190
|
+
|
191
|
+
date_entropy = function(match) {
|
192
|
+
var entropy;
|
193
|
+
if (match.year < 100) {
|
194
|
+
entropy = lg(NUM_DAYS * NUM_MONTHS * 100);
|
195
|
+
} else {
|
196
|
+
entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS);
|
197
|
+
}
|
198
|
+
if (match.separator) {
|
199
|
+
entropy += 2;
|
200
|
+
}
|
201
|
+
return entropy;
|
202
|
+
};
|
203
|
+
|
204
|
+
spatial_entropy = function(match) {
|
205
|
+
var L, S, U, d, entropy, i, j, possibilities, possible_turns, s, t, _i, _j, _k, _ref, _ref1;
|
206
|
+
if ((_ref = match.graph) === 'qwerty' || _ref === 'dvorak') {
|
207
|
+
s = KEYBOARD_STARTING_POSITIONS;
|
208
|
+
d = KEYBOARD_AVERAGE_DEGREE;
|
209
|
+
} else {
|
210
|
+
s = KEYPAD_STARTING_POSITIONS;
|
211
|
+
d = KEYPAD_AVERAGE_DEGREE;
|
212
|
+
}
|
213
|
+
possibilities = 0;
|
214
|
+
L = match.token.length;
|
215
|
+
t = match.turns;
|
216
|
+
for (i = _i = 2; 2 <= L ? _i <= L : _i >= L; i = 2 <= L ? ++_i : --_i) {
|
217
|
+
possible_turns = Math.min(t, i - 1);
|
218
|
+
for (j = _j = 1; 1 <= possible_turns ? _j <= possible_turns : _j >= possible_turns; j = 1 <= possible_turns ? ++_j : --_j) {
|
219
|
+
possibilities += nCk(i - 1, j - 1) * s * Math.pow(d, j);
|
220
|
+
}
|
221
|
+
}
|
222
|
+
entropy = lg(possibilities);
|
223
|
+
if (match.shifted_count) {
|
224
|
+
S = match.shifted_count;
|
225
|
+
U = match.token.length - match.shifted_count;
|
226
|
+
possibilities = 0;
|
227
|
+
for (i = _k = 0, _ref1 = Math.min(S, U); 0 <= _ref1 ? _k <= _ref1 : _k >= _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
|
228
|
+
possibilities += nCk(S + U, i);
|
229
|
+
}
|
230
|
+
entropy += lg(possibilities);
|
231
|
+
}
|
232
|
+
return entropy;
|
233
|
+
};
|
234
|
+
|
235
|
+
dictionary_entropy = function(match) {
|
236
|
+
match.base_entropy = lg(match.rank);
|
237
|
+
match.uppercase_entropy = extra_uppercase_entropy(match);
|
238
|
+
match.l33t_entropy = extra_l33t_entropy(match);
|
239
|
+
return match.base_entropy + match.uppercase_entropy + match.l33t_entropy;
|
240
|
+
};
|
241
|
+
|
242
|
+
START_UPPER = /^[A-Z][^A-Z]+$/;
|
243
|
+
|
244
|
+
END_UPPER = /^[^A-Z]+[A-Z]$/;
|
245
|
+
|
246
|
+
ALL_UPPER = /^[^a-z]+$/;
|
247
|
+
|
248
|
+
ALL_LOWER = /^[^A-Z]+$/;
|
249
|
+
|
250
|
+
extra_uppercase_entropy = function(match) {
|
251
|
+
var L, U, chr, i, possibilities, regex, word, _i, _j, _len, _ref, _ref1;
|
252
|
+
word = match.token;
|
253
|
+
if (word.match(ALL_LOWER)) {
|
254
|
+
return 0;
|
255
|
+
}
|
256
|
+
_ref = [START_UPPER, END_UPPER, ALL_UPPER];
|
257
|
+
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
258
|
+
regex = _ref[_i];
|
259
|
+
if (word.match(regex)) {
|
260
|
+
return 1;
|
261
|
+
}
|
262
|
+
}
|
263
|
+
U = ((function() {
|
264
|
+
var _j, _len1, _ref1, _results;
|
265
|
+
_ref1 = word.split('');
|
266
|
+
_results = [];
|
267
|
+
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
|
268
|
+
chr = _ref1[_j];
|
269
|
+
if (chr.match(/[A-Z]/)) {
|
270
|
+
_results.push(chr);
|
271
|
+
}
|
272
|
+
}
|
273
|
+
return _results;
|
274
|
+
})()).length;
|
275
|
+
L = ((function() {
|
276
|
+
var _j, _len1, _ref1, _results;
|
277
|
+
_ref1 = word.split('');
|
278
|
+
_results = [];
|
279
|
+
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
|
280
|
+
chr = _ref1[_j];
|
281
|
+
if (chr.match(/[a-z]/)) {
|
282
|
+
_results.push(chr);
|
283
|
+
}
|
284
|
+
}
|
285
|
+
return _results;
|
286
|
+
})()).length;
|
287
|
+
possibilities = 0;
|
288
|
+
for (i = _j = 0, _ref1 = Math.min(U, L); 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) {
|
289
|
+
possibilities += nCk(U + L, i);
|
290
|
+
}
|
291
|
+
return lg(possibilities);
|
292
|
+
};
|
293
|
+
|
294
|
+
extra_l33t_entropy = function(match) {
|
295
|
+
var S, U, chr, i, possibilities, subbed, unsubbed, _i, _ref, _ref1;
|
296
|
+
if (!match.l33t) {
|
297
|
+
return 0;
|
298
|
+
}
|
299
|
+
possibilities = 0;
|
300
|
+
_ref = match.sub;
|
301
|
+
for (subbed in _ref) {
|
302
|
+
unsubbed = _ref[subbed];
|
303
|
+
S = ((function() {
|
304
|
+
var _i, _len, _ref1, _results;
|
305
|
+
_ref1 = match.token.split('');
|
306
|
+
_results = [];
|
307
|
+
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
308
|
+
chr = _ref1[_i];
|
309
|
+
if (chr === subbed) {
|
310
|
+
_results.push(chr);
|
311
|
+
}
|
312
|
+
}
|
313
|
+
return _results;
|
314
|
+
})()).length;
|
315
|
+
U = ((function() {
|
316
|
+
var _i, _len, _ref1, _results;
|
317
|
+
_ref1 = match.token.split('');
|
318
|
+
_results = [];
|
319
|
+
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
320
|
+
chr = _ref1[_i];
|
321
|
+
if (chr === unsubbed) {
|
322
|
+
_results.push(chr);
|
323
|
+
}
|
324
|
+
}
|
325
|
+
return _results;
|
326
|
+
})()).length;
|
327
|
+
for (i = _i = 0, _ref1 = Math.min(U, S); 0 <= _ref1 ? _i <= _ref1 : _i >= _ref1; i = 0 <= _ref1 ? ++_i : --_i) {
|
328
|
+
possibilities += nCk(U + S, i);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
return lg(possibilities) || 1;
|
332
|
+
};
|
333
|
+
|
334
|
+
calc_bruteforce_cardinality = function(password) {
|
335
|
+
var c, chr, digits, lower, ord, symbols, upper, _i, _len, _ref, _ref1;
|
336
|
+
_ref = [false, false, false, false], lower = _ref[0], upper = _ref[1], digits = _ref[2], symbols = _ref[3];
|
337
|
+
_ref1 = password.split('');
|
338
|
+
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
339
|
+
chr = _ref1[_i];
|
340
|
+
ord = chr.charCodeAt(0);
|
341
|
+
if ((0x30 <= ord && ord <= 0x39)) {
|
342
|
+
digits = true;
|
343
|
+
} else if ((0x41 <= ord && ord <= 0x5a)) {
|
344
|
+
upper = true;
|
345
|
+
} else if ((0x61 <= ord && ord <= 0x7a)) {
|
346
|
+
lower = true;
|
347
|
+
} else {
|
348
|
+
symbols = true;
|
349
|
+
}
|
350
|
+
}
|
351
|
+
c = 0;
|
352
|
+
if (digits) {
|
353
|
+
c += 10;
|
354
|
+
}
|
355
|
+
if (upper) {
|
356
|
+
c += 26;
|
357
|
+
}
|
358
|
+
if (lower) {
|
359
|
+
c += 26;
|
360
|
+
}
|
361
|
+
if (symbols) {
|
362
|
+
c += 33;
|
363
|
+
}
|
364
|
+
return c;
|
365
|
+
};
|
366
|
+
|
367
|
+
display_time = function(seconds) {
|
368
|
+
var century, day, hour, minute, month, year;
|
369
|
+
minute = 60;
|
370
|
+
hour = minute * 60;
|
371
|
+
day = hour * 24;
|
372
|
+
month = day * 31;
|
373
|
+
year = month * 12;
|
374
|
+
century = year * 100;
|
375
|
+
if (seconds < minute) {
|
376
|
+
return 'instant';
|
377
|
+
} else if (seconds < hour) {
|
378
|
+
return "" + (1 + Math.ceil(seconds / minute)) + " minutes";
|
379
|
+
} else if (seconds < day) {
|
380
|
+
return "" + (1 + Math.ceil(seconds / hour)) + " hours";
|
381
|
+
} else if (seconds < month) {
|
382
|
+
return "" + (1 + Math.ceil(seconds / day)) + " days";
|
383
|
+
} else if (seconds < year) {
|
384
|
+
return "" + (1 + Math.ceil(seconds / month)) + " months";
|
385
|
+
} else if (seconds < century) {
|
386
|
+
return "" + (1 + Math.ceil(seconds / year)) + " years";
|
387
|
+
} else {
|
388
|
+
return 'centuries';
|
389
|
+
}
|
390
|
+
};
|