zxcvbn-ruby 1.2.0 → 1.4.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -2
  3. data/README.md +3 -12
  4. data/lib/zxcvbn/clock.rb +11 -0
  5. data/lib/zxcvbn/crack_time.rb +14 -14
  6. data/lib/zxcvbn/data.rb +42 -9
  7. data/lib/zxcvbn/dictionary_ranker.rb +7 -6
  8. data/lib/zxcvbn/entropy.rb +52 -48
  9. data/lib/zxcvbn/feedback.rb +2 -0
  10. data/lib/zxcvbn/feedback_giver.rb +4 -3
  11. data/lib/zxcvbn/match.rb +18 -4
  12. data/lib/zxcvbn/matchers/date.rb +26 -21
  13. data/lib/zxcvbn/matchers/dictionary.rb +48 -13
  14. data/lib/zxcvbn/matchers/digits.rb +3 -1
  15. data/lib/zxcvbn/matchers/l33t.rb +56 -38
  16. data/lib/zxcvbn/matchers/new_l33t.rb +30 -32
  17. data/lib/zxcvbn/matchers/regex_helpers.rb +6 -4
  18. data/lib/zxcvbn/matchers/repeat.rb +8 -8
  19. data/lib/zxcvbn/matchers/sequences.rb +18 -18
  20. data/lib/zxcvbn/matchers/spatial.rb +26 -24
  21. data/lib/zxcvbn/matchers/year.rb +4 -2
  22. data/lib/zxcvbn/math.rb +12 -8
  23. data/lib/zxcvbn/omnimatch.rb +5 -2
  24. data/lib/zxcvbn/password_strength.rb +6 -4
  25. data/lib/zxcvbn/score.rb +3 -1
  26. data/lib/zxcvbn/scorer.rb +26 -23
  27. data/lib/zxcvbn/tester.rb +2 -2
  28. data/lib/zxcvbn/trie.rb +44 -0
  29. data/lib/zxcvbn/version.rb +1 -1
  30. data/lib/zxcvbn.rb +4 -2
  31. data/sig/README.md +65 -0
  32. data/sig/zxcvbn/crack_time.rbs +13 -0
  33. data/sig/zxcvbn/data.rbs +31 -0
  34. data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
  35. data/sig/zxcvbn/entropy.rbs +33 -0
  36. data/sig/zxcvbn/feedback.rbs +8 -0
  37. data/sig/zxcvbn/feedback_giver.rbs +13 -0
  38. data/sig/zxcvbn/match.rbs +38 -0
  39. data/sig/zxcvbn/math.rbs +13 -0
  40. data/sig/zxcvbn/omnimatch.rbs +16 -0
  41. data/sig/zxcvbn/password_strength.rbs +10 -0
  42. data/sig/zxcvbn/score.rbs +15 -0
  43. data/sig/zxcvbn/scorer.rbs +20 -0
  44. data/sig/zxcvbn/tester.rbs +17 -0
  45. data/sig/zxcvbn/trie.rbs +13 -0
  46. data/sig/zxcvbn.rbs +10 -0
  47. metadata +27 -105
  48. data/.github/workflows/ci.yml +0 -23
  49. data/.gitignore +0 -18
  50. data/.rspec +0 -1
  51. data/CODE_OF_CONDUCT.md +0 -130
  52. data/Gemfile +0 -10
  53. data/Guardfile +0 -26
  54. data/Rakefile +0 -22
  55. data/spec/dictionary_ranker_spec.rb +0 -12
  56. data/spec/feedback_giver_spec.rb +0 -212
  57. data/spec/matchers/date_spec.rb +0 -109
  58. data/spec/matchers/dictionary_spec.rb +0 -30
  59. data/spec/matchers/digits_spec.rb +0 -15
  60. data/spec/matchers/l33t_spec.rb +0 -87
  61. data/spec/matchers/repeat_spec.rb +0 -18
  62. data/spec/matchers/sequences_spec.rb +0 -21
  63. data/spec/matchers/spatial_spec.rb +0 -20
  64. data/spec/matchers/year_spec.rb +0 -15
  65. data/spec/omnimatch_spec.rb +0 -24
  66. data/spec/scorer_spec.rb +0 -5
  67. data/spec/scoring/crack_time_spec.rb +0 -106
  68. data/spec/scoring/entropy_spec.rb +0 -216
  69. data/spec/scoring/math_spec.rb +0 -135
  70. data/spec/spec_helper.rb +0 -54
  71. data/spec/support/js_helpers.rb +0 -34
  72. data/spec/support/js_source/adjacency_graphs.js +0 -8
  73. data/spec/support/js_source/compiled.js +0 -1188
  74. data/spec/support/js_source/frequency_lists.js +0 -10
  75. data/spec/support/js_source/init.coffee +0 -63
  76. data/spec/support/js_source/init.js +0 -95
  77. data/spec/support/js_source/matching.coffee +0 -444
  78. data/spec/support/js_source/matching.js +0 -685
  79. data/spec/support/js_source/scoring.coffee +0 -270
  80. data/spec/support/js_source/scoring.js +0 -390
  81. data/spec/support/matcher.rb +0 -35
  82. data/spec/tester_spec.rb +0 -99
  83. data/spec/zxcvbn_spec.rb +0 -24
  84. data/zxcvbn-ruby.gemspec +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa14ec0484793f1034f5dda8b84c6265e2fc8b161e47fa323be7e78dc3223cc4
4
- data.tar.gz: e42207b60f1d6b1c4a198e6e7b466e7dcc68caaaf610bdf6e4d29a85978da58d
3
+ metadata.gz: 43a759e1040848c8da4bea8d1e871eb7d811d5cab0ebf962255990963b796f75
4
+ data.tar.gz: c78d0453bb8c92d1f3b21c37f1493f305a6667f24e6ae85a621841a9396fb584
5
5
  SHA512:
6
- metadata.gz: d6b789be21697b782a87bc8c946fed85e8f088d8097f75f2e50ae8f9f95954c01dd1df76d8eb58f031b5b6e4ee43bbb14e15264c102371cdbb20696b50e0f3c1
7
- data.tar.gz: a9a0fa505d0896c58cd1e34780ddd07a37d1d5fe34e103c09c86c4b9bdff3d8ddb78c607e2c158ef4d562f93b5a16f20e0f41b0db89dfddc1b7522c4467f1171
6
+ metadata.gz: 552d40a11c071613eefb14819b4dabbd3abc6a1f2d336e6bf9bfbc873a34640a229b3bedcc0bd77aca6ffe5e43e31fefec74fe94d74f3cdaebd4e7169610ec02
7
+ data.tar.gz: 84c006df22e0224da4a891f8fa923c81b69229f2b99e5d60ba29104473e566c83a36fbcc404defe980d80cfb176964864f00dbdfa02d655e05eaac7753e64360
data/CHANGELOG.md CHANGED
@@ -6,7 +6,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- [Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...HEAD
9
+ [Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.4.0...HEAD
10
+
11
+ ## [1.4.0] - 2026-01-15
12
+
13
+ ### Added
14
+ - RBS type signatures for improved type checking and IDE support ([#68])
15
+
16
+ ### Changed
17
+ - Minor fixups in gem metadata ([#67]).
18
+
19
+ [1.4.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.3.0...v1.4.0
20
+ [#67]: https://github.com/envato/zxcvbn-ruby/pull/67
21
+ [#68]: https://github.com/envato/zxcvbn-ruby/pull/68
22
+
23
+ ## [1.3.0] - 2026-01-02
24
+
25
+ ### Changed
26
+ - Replace OpenStruct with regular class in `Zxcvbn::Match` for 2x performance improvement ([#61])
27
+ - Implement Trie data structure for dictionary matching with 1.4x additional performance improvement ([#62])
28
+ - Replace range operators with `String#slice` for string slicing operations ([#63])
29
+ - Optimise L33t matcher with early bailout and improved deduplication ([#64])
30
+ - Pre-compute spatial graph statistics during data initialisation ([#65])
31
+ - Optimise nCk calculation using symmetry property ([#66])
32
+
33
+ Overall performance improvement: 4.1x faster than v1.2.4 (0.722ms → 0.176ms per password)
34
+
35
+ [1.3.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.4...v1.3.0
36
+ [#61]: https://github.com/envato/zxcvbn-ruby/pull/61
37
+ [#62]: https://github.com/envato/zxcvbn-ruby/pull/62
38
+ [#63]: https://github.com/envato/zxcvbn-ruby/pull/63
39
+ [#64]: https://github.com/envato/zxcvbn-ruby/pull/64
40
+ [#65]: https://github.com/envato/zxcvbn-ruby/pull/65
41
+ [#66]: https://github.com/envato/zxcvbn-ruby/pull/66
42
+
43
+ ## [1.2.4] - 2025-12-07
44
+
45
+ ### Changed
46
+ - Address security issues found by RuboCop ([#57])
47
+
48
+ [1.2.4]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.3...v1.2.4
49
+ [#57]: https://github.com/envato/zxcvbn-ruby/pull/57
50
+
51
+ ## [1.2.3] - 2025-12-07
52
+
53
+ ### Changed
54
+ - Address linting issues found by RuboCop ([#52])
55
+ - Address style issues found by RuboCop ([#53], [#54], [#55])
56
+
57
+ [1.2.3]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.2...v1.2.3
58
+ [#52]: https://github.com/envato/zxcvbn-ruby/pull/52
59
+ [#53]: https://github.com/envato/zxcvbn-ruby/pull/53
60
+ [#54]: https://github.com/envato/zxcvbn-ruby/pull/54
61
+ [#55]: https://github.com/envato/zxcvbn-ruby/pull/55
62
+
63
+ ## [1.2.2] - 2025-12-06
64
+
65
+ ### Changed
66
+ - Address layout and frozen string literal issues ([#49])
67
+
68
+ [1.2.2]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.1...v1.2.2
69
+ [#49]: https://github.com/envato/zxcvbn-ruby/pull/49
70
+
71
+ ## [1.2.1] - 2025-12-05
72
+
73
+ ### Removed
74
+ - Removed the dependency on the Ruby `benchmark` module ([#44]).
75
+ - Tests are no longer included in the gem package ([#45]).
76
+
77
+ [1.2.1]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...v1.2.1
78
+ [#44]: https://github.com/envato/zxcvbn-ruby/pull/44
79
+ [#45]: https://github.com/envato/zxcvbn-ruby/pull/45
10
80
 
11
81
  ## [1.2.0] - 2021-01-05
12
82
 
@@ -17,7 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
87
  - Use [mini\_racer] for running JavaScript specs (thanks [@RSO] ([#33]))
18
88
  - Moved CI to GitHub Actions ([#34])
19
89
 
20
- [1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...v.1.2.0
90
+ [1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...v1.2.0
21
91
  [@rso]: https://github.com/RSO
22
92
  [mini\_racer]: https://rubygems.org/gems/mini_racer/
23
93
  [#32]: https://github.com/envato/zxcvbn-ruby/pull/32
data/README.md CHANGED
@@ -119,18 +119,13 @@ $ irb
119
119
  **Note**: Storing the entropy of an encrypted or hashed value provides
120
120
  information that can make cracking the value orders of magnitude easier for an
121
121
  attacker. For this reason we advise you not to store the results of
122
- `Zxcvbn::Tester#test`. Further reading: [A Tale of Security Gone Wrong](http://gavinmiller.io/2016/a-tale-of-security-gone-wrong/).
122
+ `Zxcvbn::Tester#test`. Further reading: [A Tale of Security Gone Wrong](https://web.archive.org/web/20240715041147/http://gavinmiller.io/2016/a-tale-of-security-gone-wrong/).
123
123
 
124
124
  ## Contact
125
125
 
126
126
  - [GitHub project](https://github.com/envato/zxcvbn-ruby)
127
127
  - Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/envato/zxcvbn-ruby/issues)
128
128
 
129
- ## Maintainers
130
-
131
- - [Pete Johns](https://github.com/johnsyweb)
132
- - [Steve Hodgkiss](https://github.com/stevehodgkiss)
133
-
134
129
  ## Authors
135
130
 
136
131
  - [Steve Hodgkiss](https://github.com/stevehodgkiss)
@@ -162,18 +157,14 @@ For larger new features: Do everything as above, but first also make contact wit
162
157
 
163
158
  ## About [![code with heart by Envato](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-Envato-ff69b4.svg?style=flat-square)](https://github.com/envato/zxcvbn-ruby)
164
159
 
165
- This project is maintained by the [Envato engineering team][webuild] and funded by [Envato][envato].
166
-
167
- [<img src="http://opensource.envato.com/images/envato-oss-readme-logo.png" alt="Envato logo">][envato]
160
+ This project is maintained by the Envato engineering team and funded by [Envato][envato].
168
161
 
169
162
  Encouraging the use and creation of open source software is one of the ways we
170
- serve our community. See [our other projects][oss] or [come work with us][careers]
163
+ serve our community. Perhaps [come work with us][careers]
171
164
  where you'll find an incredibly diverse, intelligent and capable group of people
172
165
  who help make our company succeed and make our workplace fun, friendly and
173
166
  happy.
174
167
 
175
168
  [careers]: https://envato.com/careers/?utm_source=github
176
169
  [envato]: https://envato.com?utm_source=github
177
- [oss]: https://opensource.envato.com/?utm_source=github
178
- [webuild]: https://webuild.envato.com?utm_source=github
179
170
  [zxcvbn.js]: https://github.com/dropbox/zxcvbn
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zxcvbn
4
+ module Clock
5
+ def self.realtime
6
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
+ yield
8
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zxcvbn
2
4
  module CrackTime
3
5
  SINGLE_GUESS = 0.010
@@ -6,18 +8,17 @@ module Zxcvbn
6
8
  SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS
7
9
 
8
10
  def entropy_to_crack_time(entropy)
9
- 0.5 * (2 ** entropy) * SECONDS_PER_GUESS
11
+ 0.5 * (2**entropy) * SECONDS_PER_GUESS
10
12
  end
11
13
 
12
14
  def crack_time_to_score(seconds)
13
- case
14
- when seconds < 10**2
15
+ if seconds < 10**2
15
16
  0
16
- when seconds < 10**4
17
+ elsif seconds < 10**4
17
18
  1
18
- when seconds < 10**6
19
+ elsif seconds < 10**6
19
20
  2
20
- when seconds < 10**8
21
+ elsif seconds < 10**8
21
22
  3
22
23
  else
23
24
  4
@@ -32,22 +33,21 @@ module Zxcvbn
32
33
  year = month * 12
33
34
  century = year * 100
34
35
 
35
- case
36
- when seconds < minute
36
+ if seconds < minute
37
37
  'instant'
38
- when seconds < hour
38
+ elsif seconds < hour
39
39
  "#{1 + (seconds / minute).ceil} minutes"
40
- when seconds < day
40
+ elsif seconds < day
41
41
  "#{1 + (seconds / hour).ceil} hours"
42
- when seconds < month
42
+ elsif seconds < month
43
43
  "#{1 + (seconds / day).ceil} days"
44
- when seconds < year
44
+ elsif seconds < year
45
45
  "#{1 + (seconds / month).ceil} months"
46
- when seconds < century
46
+ elsif seconds < century
47
47
  "#{1 + (seconds / year).ceil} years"
48
48
  else
49
49
  'centuries'
50
50
  end
51
51
  end
52
52
  end
53
- end
53
+ end
data/lib/zxcvbn/data.rb CHANGED
@@ -1,29 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'zxcvbn/dictionary_ranker'
5
+ require 'zxcvbn/trie'
3
6
 
4
7
  module Zxcvbn
5
8
  class Data
6
9
  def initialize
7
10
  @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")
11
+ 'english' => read_word_list('english.txt'),
12
+ 'female_names' => read_word_list('female_names.txt'),
13
+ 'male_names' => read_word_list('male_names.txt'),
14
+ 'passwords' => read_word_list('passwords.txt'),
15
+ 'surnames' => read_word_list('surnames.txt')
13
16
  )
14
- @adjacency_graphs = JSON.load(DATA_PATH.join('adjacency_graphs.json').read)
17
+ @adjacency_graphs = JSON.parse(DATA_PATH.join('adjacency_graphs.json').read)
18
+ @dictionary_tries = build_tries
19
+ @graph_stats = compute_graph_stats
15
20
  end
16
21
 
17
- attr_reader :ranked_dictionaries, :adjacency_graphs
22
+ attr_reader :ranked_dictionaries, :adjacency_graphs, :dictionary_tries, :graph_stats
18
23
 
19
24
  def add_word_list(name, list)
20
- @ranked_dictionaries[name] = DictionaryRanker.rank_dictionary(list)
25
+ ranked_dict = DictionaryRanker.rank_dictionary(list)
26
+ @ranked_dictionaries[name] = ranked_dict
27
+ @dictionary_tries[name] = build_trie(ranked_dict)
21
28
  end
22
29
 
23
30
  private
24
31
 
25
32
  def read_word_list(file)
26
- DATA_PATH.join("frequency_lists", file).read.split
33
+ DATA_PATH.join('frequency_lists', file).read.split
34
+ end
35
+
36
+ def build_tries
37
+ @ranked_dictionaries.transform_values { |dict| build_trie(dict) }
38
+ end
39
+
40
+ def build_trie(ranked_dictionary)
41
+ trie = Trie.new
42
+ ranked_dictionary.each { |word, rank| trie.insert(word, rank) }
43
+ trie
44
+ end
45
+
46
+ def compute_graph_stats
47
+ stats = {}
48
+ @adjacency_graphs.each do |graph_name, graph|
49
+ degrees = graph.map { |_, neighbors| neighbors.compact.size }
50
+ sum = degrees.inject(0, :+)
51
+ average_degree = sum.to_f / graph.size
52
+ starting_positions = graph.length
53
+
54
+ stats[graph_name] = {
55
+ average_degree: average_degree,
56
+ starting_positions: starting_positions
57
+ }
58
+ end
59
+ stats
27
60
  end
28
61
  end
29
62
  end
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zxcvbn
2
4
  class DictionaryRanker
3
5
  def self.rank_dictionaries(lists)
4
- lists.each_with_object({}) do |(dict_name, words), dictionaries|
5
- dictionaries[dict_name] = rank_dictionary(words)
6
+ lists.transform_values do |words|
7
+ rank_dictionary(words)
6
8
  end
7
9
  end
8
10
 
9
11
  def self.rank_dictionary(words)
10
- words.each_with_index
11
- .with_object({}) do |(word, i), dictionary|
12
- dictionary[word.downcase] = i + 1
13
- end
12
+ words
13
+ .each_with_index
14
+ .with_object({}) { |(word, i), dictionary| dictionary[word.downcase] = i + 1 }
14
15
  end
15
16
  end
16
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/math'
2
4
 
3
5
  module Zxcvbn::Entropy
@@ -6,24 +8,25 @@ module Zxcvbn::Entropy
6
8
  def calc_entropy(match)
7
9
  return match.entropy unless match.entropy.nil?
8
10
 
9
- match.entropy = case match.pattern
10
- when 'repeat'
11
- repeat_entropy(match)
12
- when 'sequence'
13
- sequence_entropy(match)
14
- when 'digits'
15
- digits_entropy(match)
16
- when 'year'
17
- year_entropy(match)
18
- when 'date'
19
- date_entropy(match)
20
- when 'spatial'
21
- spatial_entropy(match)
22
- when 'dictionary'
23
- dictionary_entropy(match)
24
- else
25
- 0
26
- end
11
+ match.entropy =
12
+ case match.pattern
13
+ when 'repeat'
14
+ repeat_entropy(match)
15
+ when 'sequence'
16
+ sequence_entropy(match)
17
+ when 'digits'
18
+ digits_entropy(match)
19
+ when 'year'
20
+ year_entropy(match)
21
+ when 'date'
22
+ date_entropy(match)
23
+ when 'spatial'
24
+ spatial_entropy(match)
25
+ when 'dictionary'
26
+ dictionary_entropy(match)
27
+ else
28
+ 0
29
+ end
27
30
  end
28
31
 
29
32
  def repeat_entropy(match)
@@ -33,41 +36,41 @@ module Zxcvbn::Entropy
33
36
 
34
37
  def sequence_entropy(match)
35
38
  first_char = match.token[0]
36
- base_entropy = if ['a', '1'].include?(first_char)
37
- 1
38
- elsif first_char.match(/\d/)
39
- lg(10)
40
- elsif first_char.match(/[a-z]/)
41
- lg(26)
42
- else
43
- lg(26) + 1
44
- end
39
+ base_entropy =
40
+ if ['a', '1'].include?(first_char)
41
+ 1
42
+ elsif first_char.match(/\d/)
43
+ lg(10)
44
+ elsif first_char.match(/[a-z]/)
45
+ lg(26)
46
+ else
47
+ lg(26) + 1
48
+ end
45
49
  base_entropy += 1 unless match.ascending
46
50
  base_entropy + lg(match.token.length)
47
51
  end
48
52
 
49
53
  def digits_entropy(match)
50
- lg(10 ** match.token.length)
54
+ lg(10**match.token.length)
51
55
  end
52
56
 
53
57
  NUM_YEARS = 119 # years match against 1900 - 2019
54
58
  NUM_MONTHS = 12
55
59
  NUM_DAYS = 31
56
60
 
57
- def year_entropy(match)
61
+ def year_entropy(_match)
58
62
  lg(NUM_YEARS)
59
63
  end
60
64
 
61
65
  def date_entropy(match)
62
- if match.year < 100
63
- entropy = lg(NUM_DAYS * NUM_MONTHS * 100)
64
- else
65
- entropy = lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS)
66
- end
66
+ entropy =
67
+ if match.year < 100
68
+ lg(NUM_DAYS * NUM_MONTHS * 100)
69
+ else
70
+ lg(NUM_DAYS * NUM_MONTHS * NUM_YEARS)
71
+ end
67
72
 
68
- if match.separator
69
- entropy += 2
70
- end
73
+ entropy += 2 if match.separator
71
74
 
72
75
  entropy
73
76
  end
@@ -80,18 +83,18 @@ module Zxcvbn::Entropy
80
83
  match.base_entropy + match.uppercase_entropy + match.l33t_entropy
81
84
  end
82
85
 
83
- START_UPPER = /^[A-Z][^A-Z]+$/
84
- END_UPPER = /^[^A-Z]+[A-Z]$/
85
- ALL_UPPER = /^[A-Z]+$/
86
- ALL_LOWER = /^[a-z]+$/
86
+ START_UPPER = /^[A-Z][^A-Z]+$/.freeze
87
+ END_UPPER = /^[^A-Z]+[A-Z]$/.freeze
88
+ ALL_UPPER = /^[A-Z]+$/.freeze
89
+ ALL_LOWER = /^[a-z]+$/.freeze
87
90
 
88
91
  def extra_uppercase_entropy(match)
89
92
  word = match.token
90
93
  [START_UPPER, END_UPPER, ALL_UPPER].each do |regex|
91
94
  return 1 if word.match(regex)
92
95
  end
93
- num_upper = word.chars.count{|c| c.match(/[A-Z]/) }
94
- num_lower = word.chars.count{|c| c.match(/[a-z]/) }
96
+ num_upper = word.chars.count { |c| c.match(/[A-Z]/) }
97
+ num_lower = word.chars.count { |c| c.match(/[a-z]/) }
95
98
  possibilities = 0
96
99
  (0..[num_upper, num_lower].min).each do |i|
97
100
  possibilities += nCk(num_upper + num_lower, i)
@@ -102,20 +105,21 @@ module Zxcvbn::Entropy
102
105
  def extra_l33t_entropy(match)
103
106
  word = match.token
104
107
  return 0 unless match.l33t
108
+
105
109
  possibilities = 0
106
110
  match.sub.each do |subbed, unsubbed|
107
- num_subbed = word.chars.count{|c| c == subbed}
108
- num_unsubbed = word.chars.count{|c| c == unsubbed}
111
+ num_subbed = word.chars.count { |c| c == subbed }
112
+ num_unsubbed = word.chars.count { |c| c == unsubbed }
109
113
  (0..[num_subbed, num_unsubbed].min).each do |i|
110
114
  possibilities += nCk(num_subbed + num_unsubbed, i)
111
115
  end
112
116
  end
113
117
  entropy = lg(possibilities)
114
- entropy == 0 ? 1 : entropy
118
+ entropy.zero? ? 1 : entropy
115
119
  end
116
120
 
117
121
  def spatial_entropy(match)
118
- if %w|qwerty dvorak|.include? match.graph
122
+ if %w[qwerty dvorak].include? match.graph
119
123
  starting_positions = starting_positions_for_graph('qwerty')
120
124
  average_degree = average_degree_for_graph('qwerty')
121
125
  else
@@ -131,7 +135,7 @@ module Zxcvbn::Entropy
131
135
  (2..token_length).each do |i|
132
136
  possible_turns = [turns, i - 1].min
133
137
  (1..possible_turns).each do |j|
134
- possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree ** j
138
+ possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree**j
135
139
  end
136
140
  end
137
141
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zxcvbn
2
4
  class Feedback
3
5
  attr_accessor :warning, :suggestions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/entropy'
2
4
  require 'zxcvbn/feedback'
3
5
 
@@ -16,14 +18,14 @@ module Zxcvbn
16
18
 
17
19
  def self.get_feedback(score, sequence)
18
20
  # starting feedback
19
- return DEFAULT_FEEDBACK if sequence.length.zero?
21
+ return DEFAULT_FEEDBACK if sequence.empty?
20
22
 
21
23
  # no feedback if score is good or great.
22
24
  return EMPTY_FEEDBACK if score > 2
23
25
 
24
26
  # tie feedback to the longest match for longer sequences
25
27
  longest_match = sequence[0]
26
- for match in sequence[1..-1]
28
+ sequence[1..-1].each do |match|
27
29
  longest_match = match if match.token.length > longest_match.token.length
28
30
  end
29
31
 
@@ -45,7 +47,6 @@ module Zxcvbn
45
47
  get_dictionary_match_feedback match, is_sole_match
46
48
 
47
49
  when 'spatial'
48
- layout = match.graph.upcase
49
50
  warning = if match.turns == 1
50
51
  'Straight rows of keys are easy to guess'
51
52
  else
data/lib/zxcvbn/match.rb CHANGED
@@ -1,10 +1,24 @@
1
- require 'ostruct'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Zxcvbn
4
- class Match < OpenStruct
4
+ class Match
5
+ attr_accessor :pattern, :i, :j, :token, :matched_word, :rank,
6
+ :dictionary_name, :reversed, :l33t, :sub, :sub_display,
7
+ :l, :entropy, :base_entropy, :uppercase_entropy, :l33t_entropy,
8
+ :repeated_char, :sequence_name, :sequence_space, :ascending,
9
+ :graph, :turns, :shifted_count, :shiffted_count,
10
+ :year, :month, :day, :separator, :cardinality, :offset
11
+
12
+ def initialize(**attributes)
13
+ attributes.each do |key, value|
14
+ instance_variable_set("@#{key}", value)
15
+ end
16
+ end
17
+
5
18
  def to_hash
6
- @table.keys.sort.each_with_object({}) do |key, hash|
7
- hash[key.to_s] = @table[key]
19
+ instance_variables.sort.each_with_object({}) do |var, hash|
20
+ key = var.to_s.delete_prefix('@')
21
+ hash[key] = instance_variable_get(var)
8
22
  end
9
23
  end
10
24
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'zxcvbn/matchers/regex_helpers'
2
4
 
3
5
  module Zxcvbn
@@ -5,23 +7,23 @@ module Zxcvbn
5
7
  class Date
6
8
  include RegexHelpers
7
9
 
8
- YEAR_SUFFIX = /
10
+ YEAR_SUFFIX = %r{
9
11
  ( \d{1,2} ) # day or month
10
- ( \s | \- | \/ | \\ | \_ | \. ) # separator
12
+ ( \s | - | / | \\ | _ | \. ) # separator
11
13
  ( \d{1,2} ) # month or day
12
14
  \2 # same separator
13
15
  ( 19\d{2} | 200\d | 201\d | \d{2} ) # year
14
- /x
16
+ }x.freeze
15
17
 
16
- YEAR_PREFIX = /
18
+ YEAR_PREFIX = %r{
17
19
  ( 19\d{2} | 200\d | 201\d | \d{2} ) # year
18
- ( \s | - | \/ | \\ | _ | \. ) # separator
20
+ ( \s | - | / | \\ | _ | \. ) # separator
19
21
  ( \d{1,2} ) # day or month
20
22
  \2 # same separator
21
23
  ( \d{1,2} ) # month or day
22
- /x
24
+ }x.freeze
23
25
 
24
- WITHOUT_SEPARATOR = /\d{4,8}/
26
+ WITHOUT_SEPARATOR = /\d{4,8}/.freeze
25
27
 
26
28
  def matches(password)
27
29
  match_with_separator(password) + match_without_separator(password)
@@ -52,9 +54,11 @@ module Zxcvbn
52
54
 
53
55
  def match_without_separator(password)
54
56
  result = []
55
- re_match_all(WITHOUT_SEPARATOR, password) do |match, re_match|
57
+ re_match_all(WITHOUT_SEPARATOR, password) do |match, _re_match|
56
58
  extract_dates(match.token).each do |candidate|
57
- day, month, year = candidate[:day], candidate[:month], candidate[:year]
59
+ day = candidate[:day]
60
+ month = candidate[:month]
61
+ year = candidate[:year]
58
62
 
59
63
  match.pattern = 'date'
60
64
  match.day = day
@@ -71,11 +75,11 @@ module Zxcvbn
71
75
  dates = []
72
76
  date_patterns_for_length(token.length).map do |pattern|
73
77
  candidate = {
74
- :year => '',
75
- :month => '',
76
- :day => ''
78
+ year: +'',
79
+ month: +'',
80
+ day: +''
77
81
  }
78
- for i in 0...token.length
82
+ (0...token.length).each do |i|
79
83
  candidate[PATTERN_CHAR_TO_SYM[pattern[i]]] << token[i]
80
84
  end
81
85
  candidate.each do |component, value|
@@ -92,18 +96,18 @@ module Zxcvbn
92
96
  end
93
97
 
94
98
  DATE_PATTERN_FOR_LENGTH = {
95
- 8 => %w[ yyyymmdd ddmmyyyy mmddyyyy ],
96
- 7 => %w[ yyyymdd yyyymmd ddmyyyy dmmyyyy ],
97
- 6 => %w[ yymmdd ddmmyy mmddyy ],
98
- 5 => %w[ yymdd yymmd ddmyy dmmyy mmdyy mddyy ],
99
- 4 => %w[ yymd dmyy mdyy ]
100
- }
99
+ 8 => %w[yyyymmdd ddmmyyyy mmddyyyy].freeze,
100
+ 7 => %w[yyyymdd yyyymmd ddmyyyy dmmyyyy].freeze,
101
+ 6 => %w[yymmdd ddmmyy mmddyy].freeze,
102
+ 5 => %w[yymdd yymmd ddmyy dmmyy mmdyy mddyy].freeze,
103
+ 4 => %w[yymd dmyy mdyy].freeze
104
+ }.freeze
101
105
 
102
106
  PATTERN_CHAR_TO_SYM = {
103
107
  'y' => :year,
104
108
  'm' => :month,
105
109
  'd' => :day
106
- }
110
+ }.freeze
107
111
 
108
112
  def date_patterns_for_length(length)
109
113
  DATE_PATTERN_FOR_LENGTH[length] || []
@@ -112,6 +116,7 @@ module Zxcvbn
112
116
  def valid_date?(day, month, year)
113
117
  return false if day > 31 || month > 12
114
118
  return false unless year >= 1900 && year <= 2019
119
+
115
120
  true
116
121
  end
117
122
 
@@ -120,7 +125,7 @@ module Zxcvbn
120
125
  end
121
126
 
122
127
  def expand_year(year)
123
- return year
128
+ year
124
129
  # Block dates with 2 digit years for now to be compatible with the JS version
125
130
  # return year unless year < 100
126
131
  # now = Time.now.year