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.
Files changed (57) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +23 -0
  6. data/Rakefile +18 -0
  7. data/data/adjacency_graphs.json +9 -0
  8. data/data/frequency_lists.yaml +85094 -0
  9. data/lib/zxcvbn.rb +37 -0
  10. data/lib/zxcvbn/crack_time.rb +51 -0
  11. data/lib/zxcvbn/dictionary_ranker.rb +23 -0
  12. data/lib/zxcvbn/entropy.rb +151 -0
  13. data/lib/zxcvbn/match.rb +13 -0
  14. data/lib/zxcvbn/matchers/date.rb +134 -0
  15. data/lib/zxcvbn/matchers/dictionary.rb +34 -0
  16. data/lib/zxcvbn/matchers/digits.rb +18 -0
  17. data/lib/zxcvbn/matchers/l33t.rb +127 -0
  18. data/lib/zxcvbn/matchers/new_l33t.rb +120 -0
  19. data/lib/zxcvbn/matchers/regex_helpers.rb +21 -0
  20. data/lib/zxcvbn/matchers/repeat.rb +32 -0
  21. data/lib/zxcvbn/matchers/sequences.rb +64 -0
  22. data/lib/zxcvbn/matchers/spatial.rb +79 -0
  23. data/lib/zxcvbn/matchers/year.rb +18 -0
  24. data/lib/zxcvbn/math.rb +63 -0
  25. data/lib/zxcvbn/omnimatch.rb +49 -0
  26. data/lib/zxcvbn/password_strength.rb +21 -0
  27. data/lib/zxcvbn/score.rb +15 -0
  28. data/lib/zxcvbn/scorer.rb +84 -0
  29. data/lib/zxcvbn/version.rb +3 -0
  30. data/spec/matchers/date_spec.rb +109 -0
  31. data/spec/matchers/dictionary_spec.rb +14 -0
  32. data/spec/matchers/digits_spec.rb +15 -0
  33. data/spec/matchers/l33t_spec.rb +85 -0
  34. data/spec/matchers/repeat_spec.rb +18 -0
  35. data/spec/matchers/sequences_spec.rb +16 -0
  36. data/spec/matchers/spatial_spec.rb +20 -0
  37. data/spec/matchers/year_spec.rb +15 -0
  38. data/spec/omnimatch_spec.rb +24 -0
  39. data/spec/scorer_spec.rb +5 -0
  40. data/spec/scoring/crack_time_spec.rb +106 -0
  41. data/spec/scoring/entropy_spec.rb +213 -0
  42. data/spec/scoring/math_spec.rb +131 -0
  43. data/spec/spec_helper.rb +54 -0
  44. data/spec/support/js_helpers.rb +35 -0
  45. data/spec/support/js_source/adjacency_graphs.js +8 -0
  46. data/spec/support/js_source/compiled.js +1188 -0
  47. data/spec/support/js_source/frequency_lists.js +10 -0
  48. data/spec/support/js_source/init.coffee +63 -0
  49. data/spec/support/js_source/init.js +95 -0
  50. data/spec/support/js_source/matching.coffee +444 -0
  51. data/spec/support/js_source/matching.js +685 -0
  52. data/spec/support/js_source/scoring.coffee +270 -0
  53. data/spec/support/js_source/scoring.js +390 -0
  54. data/spec/support/matcher.rb +35 -0
  55. data/spec/zxcvbn_spec.rb +49 -0
  56. data/zxcvbn-ruby.gemspec +20 -0
  57. 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
+ };