zxcvbn-ruby 0.0.3 → 1.1.0

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 (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]