zxcvbn-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,18 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ class Digits
4
+ include RegexHelpers
5
+
6
+ DIGITS_REGEX = /\d{3,}/
7
+
8
+ def matches(password)
9
+ result = []
10
+ re_match_all(DIGITS_REGEX, password) do |match|
11
+ match.pattern = 'digits'
12
+ result << match
13
+ end
14
+ result
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,127 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ class L33t
4
+ L33T_TABLE = {
5
+ 'a' => ['4', '@'],
6
+ 'b' => ['8'],
7
+ 'c' => ['(', '{', '[', '<'],
8
+ 'e' => ['3'],
9
+ 'g' => ['6', '9'],
10
+ 'i' => ['1', '!', '|'],
11
+ 'l' => ['1', '|', '7'],
12
+ 'o' => ['0'],
13
+ 's' => ['$', '5'],
14
+ 't' => ['+', '7'],
15
+ 'x' => ['%'],
16
+ 'z' => ['2']
17
+ }
18
+
19
+ def initialize(dictionary_matchers)
20
+ @dictionary_matchers = dictionary_matchers
21
+ end
22
+
23
+ def matches(password)
24
+ matches = []
25
+ lowercased_password = password.downcase
26
+ combinations_to_try = l33t_subs(relevent_l33t_subtable(lowercased_password))
27
+ combinations_to_try.each do |substitution|
28
+ @dictionary_matchers.each do |matcher|
29
+ subbed_password = translate(lowercased_password, substitution)
30
+ matcher.matches(subbed_password).each do |match|
31
+ token = password[match.i..match.j]
32
+ next if token.downcase == match.matched_word.downcase
33
+ match_substitutions = {}
34
+ substitution.each do |substitution, letter|
35
+ match_substitutions[substitution] = letter if token.include?(substitution)
36
+ end
37
+ match.l33t = true
38
+ match.token = password[match.i..match.j]
39
+ match.sub = match_substitutions
40
+ match.sub_display = match_substitutions.map do |k, v|
41
+ "#{k} -> #{v}"
42
+ end.join(', ')
43
+ matches << match
44
+ end
45
+ end
46
+ end
47
+ matches
48
+ end
49
+
50
+ def translate(password, sub)
51
+ password.split('').map do |chr|
52
+ sub[chr] || chr
53
+ end.join
54
+ end
55
+
56
+ def relevent_l33t_subtable(password)
57
+ filtered = {}
58
+ L33T_TABLE.each do |letter, subs|
59
+ relevent_subs = subs.select { |s| password.include?(s) }
60
+ filtered[letter] = relevent_subs unless relevent_subs.empty?
61
+ end
62
+ filtered
63
+ end
64
+
65
+ def l33t_subs(table)
66
+ keys = table.keys
67
+ subs = [[]]
68
+ subs = find_substitutions(subs, table, keys)
69
+ new_subs = []
70
+ subs.each do |sub|
71
+ hash = {}
72
+ sub.each do |l33t_char, chr|
73
+ hash[l33t_char] = chr
74
+ end
75
+ new_subs << hash
76
+ end
77
+ new_subs
78
+ end
79
+
80
+ def find_substitutions(subs, table, keys)
81
+ return subs if keys.empty?
82
+ first_key = keys[0]
83
+ rest_keys = keys[1..-1]
84
+ next_subs = []
85
+ table[first_key].each do |l33t_char|
86
+ subs.each do |sub|
87
+ dup_l33t_index = -1
88
+ (0...sub.length).each do |i|
89
+ if sub[i][0] == l33t_char
90
+ dup_l33t_index = i
91
+ break
92
+ end
93
+ end
94
+
95
+ if dup_l33t_index == -1
96
+ sub_extension = sub + [[l33t_char, first_key]]
97
+ next_subs << sub_extension
98
+ else
99
+ sub_alternative = sub.dup
100
+ sub_alternative[dup_l33t_index, 1] = [[l33t_char, first_key]]
101
+ next_subs << sub
102
+ next_subs << sub_alternative
103
+ end
104
+ end
105
+ end
106
+ subs = dedup(next_subs)
107
+ find_substitutions(subs, table, rest_keys)
108
+ end
109
+
110
+ def dedup(subs)
111
+ deduped = []
112
+ members = []
113
+ subs.each do |sub|
114
+ assoc = sub.dup
115
+
116
+ assoc.sort! rescue debugger
117
+ label = assoc.map{|k, v| "#{k},#{v}"}.join('-')
118
+ unless members.include?(label)
119
+ members << label
120
+ deduped << sub
121
+ end
122
+ end
123
+ deduped
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,120 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ class L33t
4
+ L33T_TABLE = {
5
+ 'a' => ['4', '@'],
6
+ 'b' => ['8'],
7
+ 'c' => ['(', '{', '[', '<'],
8
+ 'e' => ['3'],
9
+ 'g' => ['6', '9'],
10
+ 'i' => ['1', '!', '|'],
11
+ 'l' => ['1', '|', '7'],
12
+ 'o' => ['0'],
13
+ 's' => ['$', '5'],
14
+ 't' => ['+', '7'],
15
+ 'x' => ['%'],
16
+ 'z' => ['2']
17
+ }
18
+
19
+ def initialize(dictionary_matchers)
20
+ @dictionary_matchers = dictionary_matchers
21
+ end
22
+
23
+ def matches(password)
24
+ matches = []
25
+ lowercased_password = password.downcase
26
+ combinations_to_try = substitution_combinations(relevant_l33t_substitutions(lowercased_password))
27
+ # debugger if password == 'abcdefghijk987654321'
28
+ combinations_to_try.each do |substitution|
29
+ @dictionary_matchers.each do |matcher|
30
+ subbed_password = substitute(lowercased_password, substitution)
31
+ matcher.matches(subbed_password).each do |match|
32
+ token = lowercased_password[match.i..match.j]
33
+ next if token == match.matched_word.downcase
34
+ # debugger if token == '1'
35
+ match_substitutions = {}
36
+ substitution.each do |letter, substitution|
37
+ match_substitutions[substitution] = letter if token.include?(substitution)
38
+ end
39
+ match.l33t = true
40
+ match.token = password[match.i..match.j]
41
+ match.sub = match_substitutions
42
+ match.sub_display = match_substitutions.map do |k, v|
43
+ "#{k} -> #{v}"
44
+ end.join(', ')
45
+ matches << match
46
+ end
47
+ end
48
+ end
49
+ matches
50
+ end
51
+
52
+ def substitute(password, substitution)
53
+ subbed_password = password.dup
54
+ substitution.each do |letter, substitution|
55
+ subbed_password.gsub!(substitution, letter)
56
+ end
57
+ subbed_password
58
+ end
59
+
60
+ # produces a l33t table of substitutions present in the given password
61
+ def relevant_l33t_substitutions(password)
62
+ subs = Hash.new do |hash, key|
63
+ hash[key] = []
64
+ end
65
+ L33T_TABLE.each do |letter, substibutions|
66
+ password.each_char do |password_char|
67
+ if substibutions.include?(password_char)
68
+ subs[letter] << password_char
69
+ end
70
+ end
71
+ end
72
+ subs
73
+ end
74
+
75
+ # takes a character substitutions hash and produces an array of all
76
+ # possible substitution combinations
77
+ def substitution_combinations(subs_hash)
78
+ combinations = []
79
+ expanded_substitutions = expanded_substitutions(subs_hash)
80
+
81
+ # build an array of all possible combinations
82
+ expanded_substitutions.each do |substitution_hash|
83
+ # convert a hash to an array of hashes with 1 key each
84
+ subs_array = substitution_hash.map do |letter, substitutions|
85
+ {letter => substitutions}
86
+ end
87
+ combinations << subs_array
88
+
89
+ # find all possible combinations for each number of combinations available
90
+ subs_array.combination(subs_array.size).each do |combination|
91
+ # Don't add duplicates
92
+ combinations << combination unless combinations.include?(combination)
93
+ end
94
+ end
95
+
96
+ # convert back to simple hash per substitution combination
97
+ combination_hashes = combinations.map do |combination_set|
98
+ hash = {}
99
+ combination_set.each do |combination_hash|
100
+ hash.merge!(combination_hash)
101
+ end
102
+ hash
103
+ end
104
+
105
+ combination_hashes
106
+ end
107
+
108
+ # expand possible combinations if multiple characters can be substituted
109
+ # e.g. {'a' => ['4', '@'], 'i' => ['1']} expands to
110
+ # [{'a' => '4', 'i' => 1}, {'a' => '@', 'i' => '1'}]
111
+ def expanded_substitutions(hash)
112
+ return {} if hash.empty?
113
+ values = hash.values
114
+ product_values = values[0].product(*values[1..-1])
115
+ product_values.map{ |p| Hash[hash.keys.zip(p)] }
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,21 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ module RegexHelpers
4
+ def re_match_all(regex, password)
5
+ loop do
6
+ re_match = regex.match(password)
7
+ break unless re_match
8
+ i, j = re_match.offset(0)
9
+ j -= 1
10
+ match = Match.new(
11
+ :i => i,
12
+ :j => j,
13
+ :token => password[i..j]
14
+ )
15
+ yield match, re_match
16
+ password = password.sub(re_match[0], ' ' * re_match[0].length)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ class Repeat
4
+ def matches(password)
5
+ result = []
6
+ i = 0
7
+ while i < password.length
8
+ j = i + 1
9
+ loop do
10
+ prev_char, cur_char = password[j-1..j]
11
+ if password[j-1] == password[j]
12
+ j += 1
13
+ else
14
+ if j - i > 2 # don't consider length 1 or 2 chains.
15
+ result << Match.new(
16
+ :pattern => 'repeat',
17
+ :i => i,
18
+ :j => j-1,
19
+ :token => password[i...j],
20
+ :repeated_char => password[i]
21
+ )
22
+ end
23
+ break
24
+ end
25
+ end
26
+ i = j
27
+ end
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ class Sequences
4
+ SEQUENCES = {
5
+ 'lower' => 'abcdefghijklmnopqrstuvwxyz',
6
+ 'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
7
+ 'digits' => '01234567890'
8
+ }
9
+
10
+ def matches(password)
11
+ result = []
12
+ i = 0
13
+ while i < password.length-1
14
+ j = i + 1
15
+ seq = nil # either lower, upper, or digits
16
+ seq_name = nil
17
+ seq_direction = nil # 1 for ascending seq abcd, -1 for dcba
18
+ SEQUENCES.each do |seq_candidate_name, seq_candidate|
19
+ seq = nil
20
+
21
+ i_n, j_n = [password[i], password[j]].map do |chr|
22
+ chr ? seq_candidate.index(chr) : nil
23
+ end
24
+
25
+ if i_n && j_n && i_n > -1 && j_n > -1
26
+ direction = j_n - i_n
27
+ if [1, -1].include?(direction)
28
+ seq = seq_candidate
29
+ seq_name = seq_candidate_name
30
+ seq_direction = direction
31
+ end
32
+ end
33
+ if seq
34
+ loop do
35
+ prev_char, cur_char = password[(j-1)], password[j]
36
+ prev_n, cur_n = [prev_char, cur_char].map do |chr|
37
+ chr ? seq_candidate.index(chr) : nil
38
+ end
39
+ if prev_n && cur_n && cur_n - prev_n == seq_direction
40
+ j += 1
41
+ else
42
+ if j - i > 2 # don't consider length 1 or 2 chains.
43
+ result << Match.new(
44
+ :pattern => 'sequence',
45
+ :i => i,
46
+ :j => j-1,
47
+ :token => password[i...j],
48
+ :sequence_name => seq_name,
49
+ :sequence_space => seq.length,
50
+ :ascending => seq_direction == 1
51
+ )
52
+ end
53
+ break
54
+ end
55
+ end
56
+ end
57
+ end
58
+ i = j
59
+ end
60
+ result
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,79 @@
1
+ module Zxcvbn
2
+ module Matchers
3
+ class Spatial
4
+ def initialize(graphs)
5
+ @graphs = graphs
6
+ end
7
+
8
+ def matches(password)
9
+ results = []
10
+ @graphs.each do |graph_name, graph|
11
+ results += matches_for_graph(graph, graph_name, password)
12
+ end
13
+ results
14
+ end
15
+
16
+ def matches_for_graph(graph, graph_name, password)
17
+ result = []
18
+ i = 0
19
+ while i < password.length - 1
20
+ j = i + 1
21
+ last_direction = nil
22
+ turns = 0
23
+ shifted_count = 0
24
+ loop do
25
+ prev_char = password[j-1]
26
+ found = false
27
+ found_direction = -1
28
+ cur_direction = -1
29
+ adjacents = graph[prev_char] || []
30
+ # consider growing pattern by one character if j hasn't gone over the edge.
31
+ if j < password.length
32
+ cur_char = password[j]
33
+ adjacents.each do |adj|
34
+ cur_direction += 1
35
+ if adj && adj.index(cur_char)
36
+ found = true
37
+ found_direction = cur_direction
38
+ if adj.index(cur_char) == 1
39
+ # index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
40
+ # for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
41
+ shifted_count += 1
42
+ end
43
+ if last_direction != found_direction
44
+ # adding a turn is correct even in the initial case when last_direction is null:
45
+ # every spatial pattern starts with a turn.
46
+ turns += 1
47
+ last_direction = found_direction
48
+ end
49
+ break
50
+ end
51
+ end
52
+ end
53
+ # if the current pattern continued, extend j and try to grow again
54
+ if found
55
+ j += 1
56
+ else
57
+ # otherwise push the pattern discovered so far, if any...
58
+ if j - i > 2 # don't consider length 1 or 2 chains.
59
+ result << Match.new(
60
+ :pattern => 'spatial',
61
+ :i => i,
62
+ :j => j-1,
63
+ :token => password[i...j],
64
+ :graph => graph_name,
65
+ :turns => turns,
66
+ :shifted_count => shifted_count
67
+ )
68
+ end
69
+ # ...and then start a new search for the rest of the password.
70
+ i = j
71
+ break
72
+ end
73
+ end
74
+ end
75
+ result
76
+ end
77
+ end
78
+ end
79
+ end