zxcvbn-ruby 0.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +12 -0
  3. data/CHANGELOG.md +42 -0
  4. data/CODE_OF_CONDUCT.md +130 -0
  5. data/Gemfile +8 -1
  6. data/Guardfile +26 -0
  7. data/{LICENSE → LICENSE.txt} +0 -0
  8. data/README.md +165 -9
  9. data/Rakefile +5 -1
  10. data/lib/zxcvbn.rb +10 -36
  11. data/lib/zxcvbn/crack_time.rb +44 -42
  12. data/lib/zxcvbn/data.rb +29 -0
  13. data/lib/zxcvbn/dictionary_ranker.rb +0 -2
  14. data/lib/zxcvbn/entropy.rb +3 -1
  15. data/lib/zxcvbn/feedback.rb +10 -0
  16. data/lib/zxcvbn/feedback_giver.rb +133 -0
  17. data/lib/zxcvbn/matchers/date.rb +2 -0
  18. data/lib/zxcvbn/matchers/dictionary.rb +2 -0
  19. data/lib/zxcvbn/matchers/digits.rb +2 -0
  20. data/lib/zxcvbn/matchers/l33t.rb +2 -2
  21. data/lib/zxcvbn/matchers/regex_helpers.rb +2 -0
  22. data/lib/zxcvbn/matchers/repeat.rb +2 -0
  23. data/lib/zxcvbn/matchers/sequences.rb +2 -0
  24. data/lib/zxcvbn/matchers/spatial.rb +2 -0
  25. data/lib/zxcvbn/matchers/year.rb +2 -0
  26. data/lib/zxcvbn/math.rb +2 -2
  27. data/lib/zxcvbn/omnimatch.rb +14 -3
  28. data/lib/zxcvbn/password_strength.rb +7 -3
  29. data/lib/zxcvbn/score.rb +1 -1
  30. data/lib/zxcvbn/scorer.rb +11 -0
  31. data/lib/zxcvbn/tester.rb +43 -0
  32. data/lib/zxcvbn/version.rb +1 -1
  33. data/spec/dictionary_ranker_spec.rb +2 -2
  34. data/spec/feedback_giver_spec.rb +212 -0
  35. data/spec/matchers/date_spec.rb +8 -8
  36. data/spec/matchers/dictionary_spec.rb +25 -14
  37. data/spec/matchers/digits_spec.rb +3 -3
  38. data/spec/matchers/l33t_spec.rb +15 -13
  39. data/spec/matchers/repeat_spec.rb +6 -6
  40. data/spec/matchers/sequences_spec.rb +5 -5
  41. data/spec/matchers/spatial_spec.rb +8 -8
  42. data/spec/matchers/year_spec.rb +3 -3
  43. data/spec/omnimatch_spec.rb +2 -2
  44. data/spec/scoring/crack_time_spec.rb +13 -13
  45. data/spec/scoring/entropy_spec.rb +28 -25
  46. data/spec/scoring/math_spec.rb +22 -18
  47. data/spec/support/matcher.rb +1 -1
  48. data/spec/tester_spec.rb +99 -0
  49. data/spec/zxcvbn_spec.rb +14 -39
  50. data/zxcvbn-ruby.gemspec +11 -0
  51. metadata +34 -29
@@ -1,51 +1,53 @@
1
- module Zxcvbn::CrackTime
2
- SINGLE_GUESS = 0.010
3
- NUM_ATTACKERS = 100
1
+ module Zxcvbn
2
+ module CrackTime
3
+ SINGLE_GUESS = 0.010
4
+ NUM_ATTACKERS = 100
4
5
 
5
- SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS
6
+ SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS
6
7
 
7
- def entropy_to_crack_time(entropy)
8
- 0.5 * (2 ** entropy) * SECONDS_PER_GUESS
9
- end
8
+ def entropy_to_crack_time(entropy)
9
+ 0.5 * (2 ** entropy) * SECONDS_PER_GUESS
10
+ end
10
11
 
11
- def crack_time_to_score(seconds)
12
- case
13
- when seconds < 10**2
14
- 0
15
- when seconds < 10**4
16
- 1
17
- when seconds < 10**6
18
- 2
19
- when seconds < 10**8
20
- 3
21
- else
22
- 4
12
+ def crack_time_to_score(seconds)
13
+ case
14
+ when seconds < 10**2
15
+ 0
16
+ when seconds < 10**4
17
+ 1
18
+ when seconds < 10**6
19
+ 2
20
+ when seconds < 10**8
21
+ 3
22
+ else
23
+ 4
24
+ end
23
25
  end
24
- end
25
26
 
26
- def display_time(seconds)
27
- minute = 60
28
- hour = minute * 60
29
- day = hour * 24
30
- month = day * 31
31
- year = month * 12
32
- century = year * 100
27
+ def display_time(seconds)
28
+ minute = 60
29
+ hour = minute * 60
30
+ day = hour * 24
31
+ month = day * 31
32
+ year = month * 12
33
+ century = year * 100
33
34
 
34
- case
35
- when seconds < minute
36
- 'instant'
37
- when seconds < hour
38
- "#{1 + (seconds / minute).ceil} minutes"
39
- when seconds < day
40
- "#{1 + (seconds / hour).ceil} hours"
41
- when seconds < month
42
- "#{1 + (seconds / day).ceil} days"
43
- when seconds < year
44
- "#{1 + (seconds / month).ceil} months"
45
- when seconds < century
46
- "#{1 + (seconds / year).ceil} years"
47
- else
48
- 'centuries'
35
+ case
36
+ when seconds < minute
37
+ 'instant'
38
+ when seconds < hour
39
+ "#{1 + (seconds / minute).ceil} minutes"
40
+ when seconds < day
41
+ "#{1 + (seconds / hour).ceil} hours"
42
+ when seconds < month
43
+ "#{1 + (seconds / day).ceil} days"
44
+ when seconds < year
45
+ "#{1 + (seconds / month).ceil} months"
46
+ when seconds < century
47
+ "#{1 + (seconds / year).ceil} years"
48
+ else
49
+ 'centuries'
50
+ end
49
51
  end
50
52
  end
51
53
  end
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+ require 'zxcvbn/dictionary_ranker'
3
+
4
+ module Zxcvbn
5
+ class Data
6
+ def initialize
7
+ @ranked_dictionaries = DictionaryRanker.rank_dictionaries(
8
+ "english" => read_word_list("english.txt"),
9
+ "female_names" => read_word_list("female_names.txt"),
10
+ "male_names" => read_word_list("male_names.txt"),
11
+ "passwords" => read_word_list("passwords.txt"),
12
+ "surnames" => read_word_list("surnames.txt")
13
+ )
14
+ @adjacency_graphs = JSON.load(DATA_PATH.join('adjacency_graphs.json').read)
15
+ end
16
+
17
+ attr_reader :ranked_dictionaries, :adjacency_graphs
18
+
19
+ def add_word_list(name, list)
20
+ @ranked_dictionaries[name] = DictionaryRanker.rank_dictionary(list)
21
+ end
22
+
23
+ private
24
+
25
+ def read_word_list(file)
26
+ DATA_PATH.join("frequency_lists", file).read.split
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Zxcvbn
4
2
  class DictionaryRanker
5
3
  def self.rank_dictionaries(lists)
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/math'
2
+
1
3
  module Zxcvbn::Entropy
2
4
  include Zxcvbn::Math
3
5
 
@@ -127,7 +129,7 @@ module Zxcvbn::Entropy
127
129
 
128
130
  # estimate the ngpumber of possible patterns w/ token length or less with number of turns or less.
129
131
  (2..token_length).each do |i|
130
- possible_turns = [turns, i -1].min
132
+ possible_turns = [turns, i - 1].min
131
133
  (1..possible_turns).each do |j|
132
134
  possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree ** j
133
135
  end
@@ -0,0 +1,10 @@
1
+ module Zxcvbn
2
+ class Feedback
3
+ attr_accessor :warning, :suggestions
4
+
5
+ def initialize(options = {})
6
+ @warning = options[:warning]
7
+ @suggestions = options[:suggestions] || []
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,133 @@
1
+ require 'zxcvbn/entropy'
2
+ require 'zxcvbn/feedback'
3
+
4
+ module Zxcvbn
5
+ class FeedbackGiver
6
+ NAME_DICTIONARIES = %w[surnames male_names female_names].freeze
7
+
8
+ DEFAULT_FEEDBACK = Feedback.new(
9
+ suggestions: [
10
+ 'Use a few words, avoid common phrases',
11
+ 'No need for symbols, digits, or uppercase letters'
12
+ ]
13
+ ).freeze
14
+
15
+ EMPTY_FEEDBACK = Feedback.new.freeze
16
+
17
+ def self.get_feedback(score, sequence)
18
+ # starting feedback
19
+ return DEFAULT_FEEDBACK if sequence.length.zero?
20
+
21
+ # no feedback if score is good or great.
22
+ return EMPTY_FEEDBACK if score > 2
23
+
24
+ # tie feedback to the longest match for longer sequences
25
+ longest_match = sequence[0]
26
+ for match in sequence[1..-1]
27
+ longest_match = match if match.token.length > longest_match.token.length
28
+ end
29
+
30
+ feedback = get_match_feedback(longest_match, sequence.length == 1)
31
+ extra_feedback = 'Add another word or two. Uncommon words are better.'
32
+
33
+ if feedback.nil?
34
+ feedback = Feedback.new(suggestions: [extra_feedback])
35
+ else
36
+ feedback.suggestions.unshift extra_feedback
37
+ end
38
+
39
+ feedback
40
+ end
41
+
42
+ def self.get_match_feedback(match, is_sole_match)
43
+ case match.pattern
44
+ when 'dictionary'
45
+ get_dictionary_match_feedback match, is_sole_match
46
+
47
+ when 'spatial'
48
+ layout = match.graph.upcase
49
+ warning = if match.turns == 1
50
+ 'Straight rows of keys are easy to guess'
51
+ else
52
+ 'Short keyboard patterns are easy to guess'
53
+ end
54
+
55
+ Feedback.new(
56
+ warning: warning,
57
+ suggestions: [
58
+ 'Use a longer keyboard pattern with more turns'
59
+ ]
60
+ )
61
+
62
+ when 'repeat'
63
+ Feedback.new(
64
+ warning: 'Repeats like "aaa" are easy to guess',
65
+ suggestions: [
66
+ 'Avoid repeated words and characters'
67
+ ]
68
+ )
69
+
70
+ when 'sequence'
71
+ Feedback.new(
72
+ warning: 'Sequences like abc or 6543 are easy to guess',
73
+ suggestions: [
74
+ 'Avoid sequences'
75
+ ]
76
+ )
77
+
78
+ when 'date'
79
+ Feedback.new(
80
+ warning: 'Dates are often easy to guess',
81
+ suggestions: [
82
+ 'Avoid dates and years that are associated with you'
83
+ ]
84
+ )
85
+ end
86
+ end
87
+
88
+ def self.get_dictionary_match_feedback(match, is_sole_match)
89
+ warning = if match.dictionary_name == 'passwords'
90
+ if is_sole_match && !match.l33t && !match.reversed
91
+ if match.rank <= 10
92
+ 'This is a top-10 common password'
93
+ elsif match.rank <= 100
94
+ 'This is a top-100 common password'
95
+ else
96
+ 'This is a very common password'
97
+ end
98
+ else
99
+ 'This is similar to a commonly used password'
100
+ end
101
+ elsif NAME_DICTIONARIES.include? match.dictionary_name
102
+ if is_sole_match
103
+ 'Names and surnames by themselves are easy to guess'
104
+ else
105
+ 'Common names and surnames are easy to guess'
106
+ end
107
+ end
108
+
109
+ suggestions = []
110
+ word = match.token
111
+
112
+ if word =~ Zxcvbn::Entropy::START_UPPER
113
+ suggestions.push "Capitalization doesn't help very much"
114
+ elsif word =~ Zxcvbn::Entropy::ALL_UPPER && word.downcase != word
115
+ suggestions.push(
116
+ 'All-uppercase is almost as easy to guess as all-lowercase'
117
+ )
118
+ end
119
+
120
+ if match.l33t
121
+ suggestions.push(
122
+ "Predictable substitutions like '@' instead of 'a' \
123
+ don't help very much"
124
+ )
125
+ end
126
+
127
+ Feedback.new(
128
+ warning: warning,
129
+ suggestions: suggestions
130
+ )
131
+ end
132
+ end
133
+ end
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/matchers/regex_helpers'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class Date
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/match'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  # Given a password and a dictionary, match on any sequential segment of
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/matchers/regex_helpers'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class Digits
@@ -31,8 +31,8 @@ module Zxcvbn
31
31
  token = password[match.i..match.j]
32
32
  next if token.downcase == match.matched_word.downcase
33
33
  match_substitutions = {}
34
- substitution.each do |substitution, letter|
35
- match_substitutions[substitution] = letter if token.include?(substitution)
34
+ substitution.each do |s, letter|
35
+ match_substitutions[s] = letter if token.include?(s)
36
36
  end
37
37
  match.l33t = true
38
38
  match.token = password[match.i..match.j]
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/match'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  module RegexHelpers
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/match'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class Repeat
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/match'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class Sequences
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/match'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class Spatial
@@ -1,3 +1,5 @@
1
+ require 'zxcvbn/matchers/regex_helpers'
2
+
1
3
  module Zxcvbn
2
4
  module Matchers
3
5
  class Year
@@ -41,14 +41,14 @@ module Zxcvbn
41
41
  end
42
42
 
43
43
  def average_degree_for_graph(graph_name)
44
- graph = Zxcvbn::ADJACENCY_GRAPHS[graph_name]
44
+ graph = data.adjacency_graphs[graph_name]
45
45
  degrees = graph.map { |_, neighbors| neighbors.compact.size }
46
46
  sum = degrees.inject(0, :+)
47
47
  sum.to_f / graph.size
48
48
  end
49
49
 
50
50
  def starting_positions_for_graph(graph_name)
51
- Zxcvbn::ADJACENCY_GRAPHS[graph_name].length
51
+ data.adjacency_graphs[graph_name].length
52
52
  end
53
53
  end
54
54
  end
@@ -1,6 +1,17 @@
1
+ require 'zxcvbn/dictionary_ranker'
2
+ require 'zxcvbn/matchers/dictionary'
3
+ require 'zxcvbn/matchers/l33t'
4
+ require 'zxcvbn/matchers/spatial'
5
+ require 'zxcvbn/matchers/digits'
6
+ require 'zxcvbn/matchers/repeat'
7
+ require 'zxcvbn/matchers/sequences'
8
+ require 'zxcvbn/matchers/year'
9
+ require 'zxcvbn/matchers/date'
10
+
1
11
  module Zxcvbn
2
12
  class Omnimatch
3
- def initialize
13
+ def initialize(data)
14
+ @data = data
4
15
  @matchers = build_matchers
5
16
  end
6
17
 
@@ -24,14 +35,14 @@ module Zxcvbn
24
35
 
25
36
  def build_matchers
26
37
  matchers = []
27
- dictionary_matchers = RANKED_DICTIONARIES.map do |name, dictionary|
38
+ dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary|
28
39
  Matchers::Dictionary.new(name, dictionary)
29
40
  end
30
41
  l33t_matcher = Matchers::L33t.new(dictionary_matchers)
31
42
  matchers += dictionary_matchers
32
43
  matchers += [
33
44
  l33t_matcher,
34
- Matchers::Spatial.new(ADJACENCY_GRAPHS),
45
+ Matchers::Spatial.new(@data.adjacency_graphs),
35
46
  Matchers::Digits.new,
36
47
  Matchers::Repeat.new,
37
48
  Matchers::Sequences.new,
@@ -1,10 +1,13 @@
1
1
  require 'benchmark'
2
+ require 'zxcvbn/feedback_giver'
3
+ require 'zxcvbn/omnimatch'
4
+ require 'zxcvbn/scorer'
2
5
 
3
6
  module Zxcvbn
4
7
  class PasswordStrength
5
- def initialize
6
- @omnimatch = Omnimatch.new
7
- @scorer = Scorer.new
8
+ def initialize(data)
9
+ @omnimatch = Omnimatch.new(data)
10
+ @scorer = Scorer.new(data)
8
11
  end
9
12
 
10
13
  def test(password, user_inputs = [])
@@ -15,6 +18,7 @@ module Zxcvbn
15
18
  result = @scorer.minimum_entropy_match_sequence(password, matches)
16
19
  end
17
20
  result.calc_time = calc_time
21
+ result.feedback = FeedbackGiver.get_feedback(result.score, result.match_sequence)
18
22
  result
19
23
  end
20
24
  end
@@ -1,7 +1,7 @@
1
1
  module Zxcvbn
2
2
  class Score
3
3
  attr_accessor :entropy, :crack_time, :crack_time_display, :score, :pattern,
4
- :match_sequence, :password, :calc_time
4
+ :match_sequence, :password, :calc_time, :feedback
5
5
 
6
6
  def initialize(options = {})
7
7
  @entropy = options[:entropy]