zxcvbn-ruby 1.1.0 → 1.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -3
- data/README.md +5 -5
- data/lib/zxcvbn/clock.rb +10 -0
- data/lib/zxcvbn/password_strength.rb +3 -3
- data/lib/zxcvbn/version.rb +3 -1
- metadata +9 -77
- data/.gitignore +0 -18
- data/.rspec +0 -1
- data/.travis.yml +0 -12
- data/CODE_OF_CONDUCT.md +0 -130
- data/Gemfile +0 -10
- data/Guardfile +0 -26
- data/Rakefile +0 -22
- data/spec/dictionary_ranker_spec.rb +0 -12
- data/spec/feedback_giver_spec.rb +0 -212
- data/spec/matchers/date_spec.rb +0 -109
- data/spec/matchers/dictionary_spec.rb +0 -30
- data/spec/matchers/digits_spec.rb +0 -15
- data/spec/matchers/l33t_spec.rb +0 -87
- data/spec/matchers/repeat_spec.rb +0 -18
- data/spec/matchers/sequences_spec.rb +0 -21
- data/spec/matchers/spatial_spec.rb +0 -20
- data/spec/matchers/year_spec.rb +0 -15
- data/spec/omnimatch_spec.rb +0 -24
- data/spec/scorer_spec.rb +0 -5
- data/spec/scoring/crack_time_spec.rb +0 -106
- data/spec/scoring/entropy_spec.rb +0 -216
- data/spec/scoring/math_spec.rb +0 -135
- data/spec/spec_helper.rb +0 -54
- data/spec/support/js_helpers.rb +0 -35
- data/spec/support/js_source/adjacency_graphs.js +0 -8
- data/spec/support/js_source/compiled.js +0 -1188
- data/spec/support/js_source/frequency_lists.js +0 -10
- data/spec/support/js_source/init.coffee +0 -63
- data/spec/support/js_source/init.js +0 -95
- data/spec/support/js_source/matching.coffee +0 -444
- data/spec/support/js_source/matching.js +0 -685
- data/spec/support/js_source/scoring.coffee +0 -270
- data/spec/support/js_source/scoring.js +0 -390
- data/spec/support/matcher.rb +0 -35
- data/spec/tester_spec.rb +0 -99
- data/spec/zxcvbn_spec.rb +0 -24
- data/zxcvbn-ruby.gemspec +0 -31
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
ranked_user_inputs_dict = {}
|
|
3
|
-
|
|
4
|
-
# initialize matcher lists
|
|
5
|
-
DICTIONARY_MATCHERS = [
|
|
6
|
-
build_dict_matcher('passwords', build_ranked_dict(passwords)),
|
|
7
|
-
build_dict_matcher('english', build_ranked_dict(english)),
|
|
8
|
-
build_dict_matcher('male_names', build_ranked_dict(male_names)),
|
|
9
|
-
build_dict_matcher('female_names', build_ranked_dict(female_names)),
|
|
10
|
-
build_dict_matcher('surnames', build_ranked_dict(surnames)),
|
|
11
|
-
build_dict_matcher('user_inputs', ranked_user_inputs_dict),
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
MATCHERS = DICTIONARY_MATCHERS.concat [
|
|
15
|
-
l33t_match,
|
|
16
|
-
digits_match, year_match, date_match,
|
|
17
|
-
repeat_match, sequence_match,
|
|
18
|
-
spatial_match
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
GRAPHS =
|
|
22
|
-
'qwerty': qwerty
|
|
23
|
-
'dvorak': dvorak
|
|
24
|
-
'keypad': keypad
|
|
25
|
-
'mac_keypad': mac_keypad
|
|
26
|
-
|
|
27
|
-
# on qwerty, 'g' has degree 6, being adjacent to 'ftyhbv'. '\' has degree 1.
|
|
28
|
-
# this calculates the average over all keys.
|
|
29
|
-
calc_average_degree = (graph) ->
|
|
30
|
-
average = 0
|
|
31
|
-
for key, neighbors of graph
|
|
32
|
-
average += (n for n in neighbors when n).length
|
|
33
|
-
average /= (k for k,v of graph).length
|
|
34
|
-
average
|
|
35
|
-
|
|
36
|
-
KEYBOARD_AVERAGE_DEGREE = calc_average_degree(qwerty)
|
|
37
|
-
KEYPAD_AVERAGE_DEGREE = calc_average_degree(keypad) # slightly different for keypad/mac keypad, but close enough
|
|
38
|
-
|
|
39
|
-
KEYBOARD_STARTING_POSITIONS = (k for k,v of qwerty).length
|
|
40
|
-
KEYPAD_STARTING_POSITIONS = (k for k,v of keypad).length
|
|
41
|
-
|
|
42
|
-
time = -> (new Date()).getTime()
|
|
43
|
-
|
|
44
|
-
# now that frequency lists are loaded, replace zxcvbn stub function.
|
|
45
|
-
zxcvbn = (password, user_inputs) ->
|
|
46
|
-
start = time()
|
|
47
|
-
if user_inputs?
|
|
48
|
-
for i in [0...user_inputs.length]
|
|
49
|
-
# update ranked_user_inputs_dict.
|
|
50
|
-
# i+1 instead of i b/c rank starts at 1.
|
|
51
|
-
ranked_user_inputs_dict[user_inputs[i]] = i + 1
|
|
52
|
-
matches = omnimatch password
|
|
53
|
-
result = minimum_entropy_match_sequence password, matches
|
|
54
|
-
result.calc_time = time() - start
|
|
55
|
-
result
|
|
56
|
-
|
|
57
|
-
# make zxcvbn function globally available
|
|
58
|
-
# via window or exports object, depending on the environment
|
|
59
|
-
if window?
|
|
60
|
-
window.zxcvbn = zxcvbn
|
|
61
|
-
window.zxcvbn_load_hook?() # run load hook from user, if defined
|
|
62
|
-
else if exports?
|
|
63
|
-
exports.zxcvbn = zxcvbn
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// Generated by CoffeeScript 1.3.3
|
|
2
|
-
var DICTIONARY_MATCHERS, GRAPHS, KEYBOARD_AVERAGE_DEGREE, KEYBOARD_STARTING_POSITIONS, KEYPAD_AVERAGE_DEGREE, KEYPAD_STARTING_POSITIONS, MATCHERS, calc_average_degree, k, ranked_user_inputs_dict, time, v, zxcvbn;
|
|
3
|
-
|
|
4
|
-
ranked_user_inputs_dict = {};
|
|
5
|
-
|
|
6
|
-
DICTIONARY_MATCHERS = [build_dict_matcher('passwords', build_ranked_dict(passwords)), build_dict_matcher('english', build_ranked_dict(english)), build_dict_matcher('male_names', build_ranked_dict(male_names)), build_dict_matcher('female_names', build_ranked_dict(female_names)), build_dict_matcher('surnames', build_ranked_dict(surnames)), build_dict_matcher('user_inputs', ranked_user_inputs_dict)];
|
|
7
|
-
|
|
8
|
-
MATCHERS = DICTIONARY_MATCHERS.concat([l33t_match, digits_match, year_match, date_match, repeat_match, sequence_match, spatial_match]);
|
|
9
|
-
|
|
10
|
-
GRAPHS = {
|
|
11
|
-
'qwerty': qwerty,
|
|
12
|
-
'dvorak': dvorak,
|
|
13
|
-
'keypad': keypad,
|
|
14
|
-
'mac_keypad': mac_keypad
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
calc_average_degree = function(graph) {
|
|
18
|
-
var average, k, key, n, neighbors, v;
|
|
19
|
-
average = 0;
|
|
20
|
-
for (key in graph) {
|
|
21
|
-
neighbors = graph[key];
|
|
22
|
-
average += ((function() {
|
|
23
|
-
var _i, _len, _results;
|
|
24
|
-
_results = [];
|
|
25
|
-
for (_i = 0, _len = neighbors.length; _i < _len; _i++) {
|
|
26
|
-
n = neighbors[_i];
|
|
27
|
-
if (n) {
|
|
28
|
-
_results.push(n);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return _results;
|
|
32
|
-
})()).length;
|
|
33
|
-
}
|
|
34
|
-
average /= ((function() {
|
|
35
|
-
var _results;
|
|
36
|
-
_results = [];
|
|
37
|
-
for (k in graph) {
|
|
38
|
-
v = graph[k];
|
|
39
|
-
_results.push(k);
|
|
40
|
-
}
|
|
41
|
-
return _results;
|
|
42
|
-
})()).length;
|
|
43
|
-
return average;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
KEYBOARD_AVERAGE_DEGREE = calc_average_degree(qwerty);
|
|
47
|
-
|
|
48
|
-
KEYPAD_AVERAGE_DEGREE = calc_average_degree(keypad);
|
|
49
|
-
|
|
50
|
-
KEYBOARD_STARTING_POSITIONS = ((function() {
|
|
51
|
-
var _results;
|
|
52
|
-
_results = [];
|
|
53
|
-
for (k in qwerty) {
|
|
54
|
-
v = qwerty[k];
|
|
55
|
-
_results.push(k);
|
|
56
|
-
}
|
|
57
|
-
return _results;
|
|
58
|
-
})()).length;
|
|
59
|
-
|
|
60
|
-
KEYPAD_STARTING_POSITIONS = ((function() {
|
|
61
|
-
var _results;
|
|
62
|
-
_results = [];
|
|
63
|
-
for (k in keypad) {
|
|
64
|
-
v = keypad[k];
|
|
65
|
-
_results.push(k);
|
|
66
|
-
}
|
|
67
|
-
return _results;
|
|
68
|
-
})()).length;
|
|
69
|
-
|
|
70
|
-
time = function() {
|
|
71
|
-
return (new Date()).getTime();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
zxcvbn = function(password, user_inputs) {
|
|
75
|
-
var i, matches, result, start, _i, _ref;
|
|
76
|
-
start = time();
|
|
77
|
-
if (user_inputs != null) {
|
|
78
|
-
for (i = _i = 0, _ref = user_inputs.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
|
|
79
|
-
ranked_user_inputs_dict[user_inputs[i]] = i + 1;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
matches = omnimatch(password);
|
|
83
|
-
result = minimum_entropy_match_sequence(password, matches);
|
|
84
|
-
result.calc_time = time() - start;
|
|
85
|
-
return result;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
if (typeof window !== "undefined" && window !== null) {
|
|
89
|
-
window.zxcvbn = zxcvbn;
|
|
90
|
-
if (typeof window.zxcvbn_load_hook === "function") {
|
|
91
|
-
window.zxcvbn_load_hook();
|
|
92
|
-
}
|
|
93
|
-
} else if (typeof exports !== "undefined" && exports !== null) {
|
|
94
|
-
exports.zxcvbn = zxcvbn;
|
|
95
|
-
}
|
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
empty = (obj) -> (k for k of obj).length == 0
|
|
3
|
-
extend = (lst, lst2) -> lst.push.apply lst, lst2
|
|
4
|
-
translate = (string, chr_map) -> (chr_map[chr] or chr for chr in string.split('')).join('')
|
|
5
|
-
|
|
6
|
-
# ------------------------------------------------------------------------------
|
|
7
|
-
# omnimatch -- combine everything ----------------------------------------------
|
|
8
|
-
# ------------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
omnimatch = (password) ->
|
|
11
|
-
matches = []
|
|
12
|
-
for matcher in MATCHERS
|
|
13
|
-
extend matches, matcher(password)
|
|
14
|
-
matches.sort (match1, match2) ->
|
|
15
|
-
(match1.i - match2.i) or (match1.j - match2.j)
|
|
16
|
-
|
|
17
|
-
#-------------------------------------------------------------------------------
|
|
18
|
-
# dictionary match (common passwords, english, last names, etc) ----------------
|
|
19
|
-
#-------------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
dictionary_match = (password, ranked_dict) ->
|
|
22
|
-
result = []
|
|
23
|
-
len = password.length
|
|
24
|
-
password_lower = password.toLowerCase()
|
|
25
|
-
for i in [0...len]
|
|
26
|
-
for j in [i...len]
|
|
27
|
-
if password_lower[i..j] of ranked_dict
|
|
28
|
-
word = password_lower[i..j]
|
|
29
|
-
rank = ranked_dict[word]
|
|
30
|
-
result.push(
|
|
31
|
-
pattern: 'dictionary'
|
|
32
|
-
i: i
|
|
33
|
-
j: j
|
|
34
|
-
token: password[i..j]
|
|
35
|
-
matched_word: word
|
|
36
|
-
rank: rank
|
|
37
|
-
)
|
|
38
|
-
result
|
|
39
|
-
|
|
40
|
-
build_ranked_dict = (unranked_list) ->
|
|
41
|
-
result = {}
|
|
42
|
-
i = 1 # rank starts at 1, not 0
|
|
43
|
-
for word in unranked_list
|
|
44
|
-
result[word] = i
|
|
45
|
-
i += 1
|
|
46
|
-
result
|
|
47
|
-
|
|
48
|
-
build_dict_matcher = (dict_name, ranked_dict) ->
|
|
49
|
-
(password) ->
|
|
50
|
-
matches = dictionary_match(password, ranked_dict)
|
|
51
|
-
match.dictionary_name = dict_name for match in matches
|
|
52
|
-
matches
|
|
53
|
-
|
|
54
|
-
#-------------------------------------------------------------------------------
|
|
55
|
-
# dictionary match with common l33t substitutions ------------------------------
|
|
56
|
-
#-------------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
l33t_table =
|
|
59
|
-
a: ['4', '@']
|
|
60
|
-
b: ['8']
|
|
61
|
-
c: ['(', '{', '[', '<']
|
|
62
|
-
e: ['3']
|
|
63
|
-
g: ['6', '9']
|
|
64
|
-
i: ['1', '!', '|']
|
|
65
|
-
l: ['1', '|', '7']
|
|
66
|
-
o: ['0']
|
|
67
|
-
s: ['$', '5']
|
|
68
|
-
t: ['+', '7']
|
|
69
|
-
x: ['%']
|
|
70
|
-
z: ['2']
|
|
71
|
-
|
|
72
|
-
# makes a pruned copy of l33t_table that only includes password's possible substitutions
|
|
73
|
-
relevent_l33t_subtable = (password) ->
|
|
74
|
-
password_chars = {}
|
|
75
|
-
for chr in password.split('')
|
|
76
|
-
password_chars[chr] = true
|
|
77
|
-
filtered = {}
|
|
78
|
-
for letter, subs of l33t_table
|
|
79
|
-
relevent_subs = (sub for sub in subs when sub of password_chars)
|
|
80
|
-
if relevent_subs.length > 0
|
|
81
|
-
filtered[letter] = relevent_subs
|
|
82
|
-
filtered
|
|
83
|
-
|
|
84
|
-
# returns the list of possible 1337 replacement dictionaries for a given password
|
|
85
|
-
enumerate_l33t_subs = (table) ->
|
|
86
|
-
keys = (k for k of table)
|
|
87
|
-
subs = [[]]
|
|
88
|
-
|
|
89
|
-
dedup = (subs) ->
|
|
90
|
-
deduped = []
|
|
91
|
-
members = {}
|
|
92
|
-
for sub in subs
|
|
93
|
-
assoc = ([k,v] for k,v in sub)
|
|
94
|
-
assoc.sort()
|
|
95
|
-
label = (k+','+v for k,v in assoc).join('-')
|
|
96
|
-
unless label of members
|
|
97
|
-
members[label] = true
|
|
98
|
-
deduped.push sub
|
|
99
|
-
deduped
|
|
100
|
-
|
|
101
|
-
helper = (keys) ->
|
|
102
|
-
return if not keys.length
|
|
103
|
-
first_key = keys[0]
|
|
104
|
-
rest_keys = keys[1..]
|
|
105
|
-
next_subs = []
|
|
106
|
-
for l33t_chr in table[first_key]
|
|
107
|
-
for sub in subs
|
|
108
|
-
dup_l33t_index = -1
|
|
109
|
-
for i in [0...sub.length]
|
|
110
|
-
if sub[i][0] == l33t_chr
|
|
111
|
-
dup_l33t_index = i
|
|
112
|
-
break
|
|
113
|
-
if dup_l33t_index == -1
|
|
114
|
-
sub_extension = sub.concat [[l33t_chr, first_key]]
|
|
115
|
-
next_subs.push sub_extension
|
|
116
|
-
else
|
|
117
|
-
sub_alternative = sub.slice(0)
|
|
118
|
-
sub_alternative.splice(dup_l33t_index, 1)
|
|
119
|
-
sub_alternative.push [l33t_chr, first_key]
|
|
120
|
-
next_subs.push sub
|
|
121
|
-
next_subs.push sub_alternative
|
|
122
|
-
subs = dedup next_subs
|
|
123
|
-
helper(rest_keys)
|
|
124
|
-
|
|
125
|
-
helper(keys)
|
|
126
|
-
sub_dicts = [] # convert from assoc lists to dicts
|
|
127
|
-
for sub in subs
|
|
128
|
-
sub_dict = {}
|
|
129
|
-
for [l33t_chr, chr] in sub
|
|
130
|
-
sub_dict[l33t_chr] = chr
|
|
131
|
-
sub_dicts.push sub_dict
|
|
132
|
-
sub_dicts
|
|
133
|
-
|
|
134
|
-
l33t_match = (password) ->
|
|
135
|
-
matches = []
|
|
136
|
-
for sub in enumerate_l33t_subs relevent_l33t_subtable password
|
|
137
|
-
break if empty sub # corner case: password has no relevent subs.
|
|
138
|
-
for matcher in DICTIONARY_MATCHERS
|
|
139
|
-
subbed_password = translate password, sub
|
|
140
|
-
for match in matcher(subbed_password)
|
|
141
|
-
token = password[match.i..match.j]
|
|
142
|
-
if token.toLowerCase() == match.matched_word
|
|
143
|
-
continue # only return the matches that contain an actual substitution
|
|
144
|
-
match_sub = {} # subset of mappings in sub that are in use for this match
|
|
145
|
-
for subbed_chr, chr of sub when token.indexOf(subbed_chr) != -1
|
|
146
|
-
match_sub[subbed_chr] = chr
|
|
147
|
-
match.l33t = true
|
|
148
|
-
match.token = token
|
|
149
|
-
match.sub = match_sub
|
|
150
|
-
match.sub_display = ("#{k} -> #{v}" for k,v of match_sub).join(', ')
|
|
151
|
-
matches.push match
|
|
152
|
-
matches
|
|
153
|
-
|
|
154
|
-
# ------------------------------------------------------------------------------
|
|
155
|
-
# spatial match (qwerty/dvorak/keypad) -----------------------------------------
|
|
156
|
-
# ------------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
spatial_match = (password) ->
|
|
159
|
-
matches = []
|
|
160
|
-
for graph_name, graph of GRAPHS
|
|
161
|
-
extend matches, spatial_match_helper(password, graph, graph_name)
|
|
162
|
-
matches
|
|
163
|
-
|
|
164
|
-
spatial_match_helper = (password, graph, graph_name) ->
|
|
165
|
-
result = []
|
|
166
|
-
i = 0
|
|
167
|
-
while i < password.length - 1
|
|
168
|
-
j = i + 1
|
|
169
|
-
last_direction = null
|
|
170
|
-
turns = 0
|
|
171
|
-
shifted_count = 0
|
|
172
|
-
loop
|
|
173
|
-
prev_char = password.charAt(j-1)
|
|
174
|
-
found = false
|
|
175
|
-
found_direction = -1
|
|
176
|
-
cur_direction = -1
|
|
177
|
-
adjacents = graph[prev_char] or []
|
|
178
|
-
# consider growing pattern by one character if j hasn't gone over the edge.
|
|
179
|
-
if j < password.length
|
|
180
|
-
cur_char = password.charAt(j)
|
|
181
|
-
for adj in adjacents
|
|
182
|
-
cur_direction += 1
|
|
183
|
-
if adj and adj.indexOf(cur_char) != -1
|
|
184
|
-
found = true
|
|
185
|
-
found_direction = cur_direction
|
|
186
|
-
if adj.indexOf(cur_char) == 1
|
|
187
|
-
# index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
|
|
188
|
-
# for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
|
|
189
|
-
shifted_count += 1
|
|
190
|
-
if last_direction != found_direction
|
|
191
|
-
# adding a turn is correct even in the initial case when last_direction is null:
|
|
192
|
-
# every spatial pattern starts with a turn.
|
|
193
|
-
turns += 1
|
|
194
|
-
last_direction = found_direction
|
|
195
|
-
break
|
|
196
|
-
# if the current pattern continued, extend j and try to grow again
|
|
197
|
-
if found
|
|
198
|
-
j += 1
|
|
199
|
-
# otherwise push the pattern discovered so far, if any...
|
|
200
|
-
else
|
|
201
|
-
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
202
|
-
result.push
|
|
203
|
-
pattern: 'spatial'
|
|
204
|
-
i: i
|
|
205
|
-
j: j-1
|
|
206
|
-
token: password[i...j]
|
|
207
|
-
graph: graph_name
|
|
208
|
-
turns: turns
|
|
209
|
-
shifted_count: shifted_count
|
|
210
|
-
# ...and then start a new search for the rest of the password.
|
|
211
|
-
i = j
|
|
212
|
-
break
|
|
213
|
-
result
|
|
214
|
-
|
|
215
|
-
#-------------------------------------------------------------------------------
|
|
216
|
-
# repeats (aaa) and sequences (abcdef) -----------------------------------------
|
|
217
|
-
#-------------------------------------------------------------------------------
|
|
218
|
-
|
|
219
|
-
repeat_match = (password) ->
|
|
220
|
-
result = []
|
|
221
|
-
i = 0
|
|
222
|
-
while i < password.length
|
|
223
|
-
j = i + 1
|
|
224
|
-
loop
|
|
225
|
-
[prev_char, cur_char] = password[j-1..j]
|
|
226
|
-
if password.charAt(j-1) == password.charAt(j)
|
|
227
|
-
j += 1
|
|
228
|
-
else
|
|
229
|
-
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
230
|
-
result.push
|
|
231
|
-
pattern: 'repeat'
|
|
232
|
-
i: i
|
|
233
|
-
j: j-1
|
|
234
|
-
token: password[i...j]
|
|
235
|
-
repeated_char: password.charAt(i)
|
|
236
|
-
break
|
|
237
|
-
i = j
|
|
238
|
-
result
|
|
239
|
-
|
|
240
|
-
SEQUENCES =
|
|
241
|
-
lower: 'abcdefghijklmnopqrstuvwxyz'
|
|
242
|
-
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
243
|
-
digits: '01234567890'
|
|
244
|
-
|
|
245
|
-
sequence_match = (password) ->
|
|
246
|
-
result = []
|
|
247
|
-
i = 0
|
|
248
|
-
while i < password.length
|
|
249
|
-
j = i + 1
|
|
250
|
-
seq = null # either lower, upper, or digits
|
|
251
|
-
seq_name = null
|
|
252
|
-
seq_direction = null # 1 for ascending seq abcd, -1 for dcba
|
|
253
|
-
for seq_candidate_name, seq_candidate of SEQUENCES
|
|
254
|
-
[i_n, j_n] = (seq_candidate.indexOf(chr) for chr in [password.charAt(i), password.charAt(j)])
|
|
255
|
-
if i_n > -1 and j_n > -1
|
|
256
|
-
direction = j_n - i_n
|
|
257
|
-
if direction in [1, -1]
|
|
258
|
-
seq = seq_candidate
|
|
259
|
-
seq_name = seq_candidate_name
|
|
260
|
-
seq_direction = direction
|
|
261
|
-
break
|
|
262
|
-
if seq
|
|
263
|
-
loop
|
|
264
|
-
[prev_char, cur_char] = password[j-1..j]
|
|
265
|
-
[prev_n, cur_n] = (seq_candidate.indexOf(chr) for chr in [prev_char, cur_char])
|
|
266
|
-
# Bug fix. 'ba+' was falsly being reported as a sequence due to 1 - null working in JS.
|
|
267
|
-
# TODO: Submit PR to zxcvbn.js
|
|
268
|
-
if !!cur_n && (cur_n - prev_n == seq_direction)
|
|
269
|
-
j += 1
|
|
270
|
-
else
|
|
271
|
-
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
272
|
-
result.push
|
|
273
|
-
pattern: 'sequence'
|
|
274
|
-
i: i
|
|
275
|
-
j: j-1
|
|
276
|
-
token: password[i...j]
|
|
277
|
-
sequence_name: seq_name
|
|
278
|
-
sequence_space: seq.length
|
|
279
|
-
ascending: seq_direction == 1
|
|
280
|
-
break
|
|
281
|
-
i = j
|
|
282
|
-
result
|
|
283
|
-
|
|
284
|
-
#-------------------------------------------------------------------------------
|
|
285
|
-
# digits, years, dates ---------------------------------------------------------
|
|
286
|
-
#-------------------------------------------------------------------------------
|
|
287
|
-
|
|
288
|
-
repeat = (chr, n) -> (chr for i in [1..n]).join('')
|
|
289
|
-
|
|
290
|
-
findall = (password, rx) ->
|
|
291
|
-
matches = []
|
|
292
|
-
loop
|
|
293
|
-
match = password.match rx
|
|
294
|
-
break if not match
|
|
295
|
-
match.i = match.index
|
|
296
|
-
match.j = match.index + match[0].length - 1
|
|
297
|
-
matches.push match
|
|
298
|
-
password = password.replace match[0], repeat(' ', match[0].length)
|
|
299
|
-
matches
|
|
300
|
-
|
|
301
|
-
digits_rx = /\d{3,}/
|
|
302
|
-
digits_match = (password) ->
|
|
303
|
-
for match in findall password, digits_rx
|
|
304
|
-
[i, j] = [match.i, match.j]
|
|
305
|
-
pattern: 'digits'
|
|
306
|
-
i: i
|
|
307
|
-
j: j
|
|
308
|
-
token: password[i..j]
|
|
309
|
-
|
|
310
|
-
# 4-digit years only. 2-digit years have the same entropy as 2-digit brute force.
|
|
311
|
-
year_rx = /19\d\d|200\d|201\d/
|
|
312
|
-
year_match = (password) ->
|
|
313
|
-
for match in findall password, year_rx
|
|
314
|
-
[i, j] = [match.i, match.j]
|
|
315
|
-
pattern: 'year'
|
|
316
|
-
i: i
|
|
317
|
-
j: j
|
|
318
|
-
token: password[i..j]
|
|
319
|
-
|
|
320
|
-
date_match = (password) ->
|
|
321
|
-
# match dates with separators 1/1/1911 and dates without 111997
|
|
322
|
-
date_without_sep_match(password).concat date_sep_match(password)
|
|
323
|
-
|
|
324
|
-
date_without_sep_match = (password) ->
|
|
325
|
-
date_matches = []
|
|
326
|
-
for digit_match in findall password, /\d{4,8}/ # 1197 is length-4, 01011997 is length 8
|
|
327
|
-
[i, j] = [digit_match.i, digit_match.j]
|
|
328
|
-
token = password[i..j]
|
|
329
|
-
end = token.length
|
|
330
|
-
candidates_round_1 = [] # parse year alternatives
|
|
331
|
-
if token.length <= 6
|
|
332
|
-
candidates_round_1.push # 2-digit year prefix
|
|
333
|
-
daymonth: token[2..]
|
|
334
|
-
year: token[0..1]
|
|
335
|
-
i: i
|
|
336
|
-
j: j
|
|
337
|
-
candidates_round_1.push # 2-digit year suffix
|
|
338
|
-
daymonth: token[0...end-2]
|
|
339
|
-
year: token[end-2..]
|
|
340
|
-
i: i
|
|
341
|
-
j: j
|
|
342
|
-
if token.length >= 6
|
|
343
|
-
candidates_round_1.push # 4-digit year prefix
|
|
344
|
-
daymonth: token[4..]
|
|
345
|
-
year: token[0..3]
|
|
346
|
-
i: i
|
|
347
|
-
j: j
|
|
348
|
-
candidates_round_1.push # 4-digit year suffix
|
|
349
|
-
daymonth: token[0...end-4]
|
|
350
|
-
year: token[end-4..]
|
|
351
|
-
i: i
|
|
352
|
-
j: j
|
|
353
|
-
candidates_round_2 = [] # parse day/month alternatives
|
|
354
|
-
for candidate in candidates_round_1
|
|
355
|
-
switch candidate.daymonth.length
|
|
356
|
-
when 2 # ex. 1 1 97
|
|
357
|
-
candidates_round_2.push
|
|
358
|
-
day: candidate.daymonth[0]
|
|
359
|
-
month: candidate.daymonth[1]
|
|
360
|
-
year: candidate.year
|
|
361
|
-
i: candidate.i
|
|
362
|
-
j: candidate.j
|
|
363
|
-
when 3 # ex. 11 1 97 or 1 11 97
|
|
364
|
-
candidates_round_2.push
|
|
365
|
-
day: candidate.daymonth[0..1]
|
|
366
|
-
month: candidate.daymonth[2]
|
|
367
|
-
year: candidate.year
|
|
368
|
-
i: candidate.i
|
|
369
|
-
j: candidate.j
|
|
370
|
-
candidates_round_2.push
|
|
371
|
-
day: candidate.daymonth[0]
|
|
372
|
-
month: candidate.daymonth[1..2]
|
|
373
|
-
year: candidate.year
|
|
374
|
-
i: candidate.i
|
|
375
|
-
j: candidate.j
|
|
376
|
-
when 4 # ex. 11 11 97
|
|
377
|
-
candidates_round_2.push
|
|
378
|
-
day: candidate.daymonth[0..1]
|
|
379
|
-
month: candidate.daymonth[2..3]
|
|
380
|
-
year: candidate.year
|
|
381
|
-
i: candidate.i
|
|
382
|
-
j: candidate.j
|
|
383
|
-
# final loop: reject invalid dates
|
|
384
|
-
for candidate in candidates_round_2
|
|
385
|
-
day = parseInt(candidate.day)
|
|
386
|
-
month = parseInt(candidate.month)
|
|
387
|
-
year = parseInt(candidate.year)
|
|
388
|
-
[valid, [day, month, year]] = check_date(day, month, year)
|
|
389
|
-
continue unless valid
|
|
390
|
-
date_matches.push
|
|
391
|
-
pattern: 'date'
|
|
392
|
-
i: candidate.i
|
|
393
|
-
j: candidate.j
|
|
394
|
-
token: password[i..j]
|
|
395
|
-
separator: ''
|
|
396
|
-
day: day
|
|
397
|
-
month: month
|
|
398
|
-
year: year
|
|
399
|
-
date_matches
|
|
400
|
-
|
|
401
|
-
date_rx_year_suffix = ///
|
|
402
|
-
( \d{1,2} ) # day or month
|
|
403
|
-
( \s | - | / | \\ | _ | \. ) # separator
|
|
404
|
-
( \d{1,2} ) # month or day
|
|
405
|
-
\2 # same separator
|
|
406
|
-
( 19\d{2} | 200\d | 201\d | \d{2} ) # year
|
|
407
|
-
///
|
|
408
|
-
date_rx_year_prefix = ///
|
|
409
|
-
( 19\d{2} | 200\d | 201\d | \d{2} ) # year
|
|
410
|
-
( \s | - | / | \\ | _ | \. ) # separator
|
|
411
|
-
( \d{1,2} ) # day or month
|
|
412
|
-
\2 # same separator
|
|
413
|
-
( \d{1,2} ) # month or day
|
|
414
|
-
///
|
|
415
|
-
date_sep_match = (password) ->
|
|
416
|
-
matches = []
|
|
417
|
-
for match in findall password, date_rx_year_suffix
|
|
418
|
-
[match.day, match.month, match.year] = (parseInt(match[k]) for k in [1,3,4])
|
|
419
|
-
match.sep = match[2]
|
|
420
|
-
matches.push match
|
|
421
|
-
for match in findall password, date_rx_year_prefix
|
|
422
|
-
[match.day, match.month, match.year] = (parseInt(match[k]) for k in [4,3,1])
|
|
423
|
-
match.sep = match[2]
|
|
424
|
-
matches.push match
|
|
425
|
-
for match in matches
|
|
426
|
-
[valid, [day, month, year]] = check_date(match.day, match.month, match.year)
|
|
427
|
-
continue unless valid
|
|
428
|
-
pattern: 'date'
|
|
429
|
-
i: match.i
|
|
430
|
-
j: match.j
|
|
431
|
-
token: password[match.i..match.j]
|
|
432
|
-
separator: match.sep
|
|
433
|
-
day: day
|
|
434
|
-
month: month
|
|
435
|
-
year: year
|
|
436
|
-
|
|
437
|
-
check_date = (day, month, year) ->
|
|
438
|
-
if 12 <= month <= 31 and day <= 12 # tolerate both day-month and month-day order
|
|
439
|
-
[day, month] = [month, day]
|
|
440
|
-
if day > 31 or month > 12
|
|
441
|
-
return [false, []]
|
|
442
|
-
unless 1900 <= year <= 2019
|
|
443
|
-
return [false, []]
|
|
444
|
-
[true, [day, month, year]]
|