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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -2
- data/README.md +3 -12
- data/lib/zxcvbn/clock.rb +11 -0
- data/lib/zxcvbn/crack_time.rb +14 -14
- data/lib/zxcvbn/data.rb +42 -9
- data/lib/zxcvbn/dictionary_ranker.rb +7 -6
- data/lib/zxcvbn/entropy.rb +52 -48
- data/lib/zxcvbn/feedback.rb +2 -0
- data/lib/zxcvbn/feedback_giver.rb +4 -3
- data/lib/zxcvbn/match.rb +18 -4
- data/lib/zxcvbn/matchers/date.rb +26 -21
- data/lib/zxcvbn/matchers/dictionary.rb +48 -13
- data/lib/zxcvbn/matchers/digits.rb +3 -1
- data/lib/zxcvbn/matchers/l33t.rb +56 -38
- data/lib/zxcvbn/matchers/new_l33t.rb +30 -32
- data/lib/zxcvbn/matchers/regex_helpers.rb +6 -4
- data/lib/zxcvbn/matchers/repeat.rb +8 -8
- data/lib/zxcvbn/matchers/sequences.rb +18 -18
- data/lib/zxcvbn/matchers/spatial.rb +26 -24
- data/lib/zxcvbn/matchers/year.rb +4 -2
- data/lib/zxcvbn/math.rb +12 -8
- data/lib/zxcvbn/omnimatch.rb +5 -2
- data/lib/zxcvbn/password_strength.rb +6 -4
- data/lib/zxcvbn/score.rb +3 -1
- data/lib/zxcvbn/scorer.rb +26 -23
- data/lib/zxcvbn/tester.rb +2 -2
- data/lib/zxcvbn/trie.rb +44 -0
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +4 -2
- data/sig/README.md +65 -0
- data/sig/zxcvbn/crack_time.rbs +13 -0
- data/sig/zxcvbn/data.rbs +31 -0
- data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
- data/sig/zxcvbn/entropy.rbs +33 -0
- data/sig/zxcvbn/feedback.rbs +8 -0
- data/sig/zxcvbn/feedback_giver.rbs +13 -0
- data/sig/zxcvbn/match.rbs +38 -0
- data/sig/zxcvbn/math.rbs +13 -0
- data/sig/zxcvbn/omnimatch.rbs +16 -0
- data/sig/zxcvbn/password_strength.rbs +10 -0
- data/sig/zxcvbn/score.rbs +15 -0
- data/sig/zxcvbn/scorer.rbs +20 -0
- data/sig/zxcvbn/tester.rbs +17 -0
- data/sig/zxcvbn/trie.rbs +13 -0
- data/sig/zxcvbn.rbs +10 -0
- metadata +27 -105
- data/.github/workflows/ci.yml +0 -23
- data/.gitignore +0 -18
- data/.rspec +0 -1
- 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 -34
- 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 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43a759e1040848c8da4bea8d1e871eb7d811d5cab0ebf962255990963b796f75
|
|
4
|
+
data.tar.gz: c78d0453bb8c92d1f3b21c37f1493f305a6667f24e6ae85a621841a9396fb584
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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...
|
|
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 [](https://github.com/envato/zxcvbn-ruby)
|
|
164
159
|
|
|
165
|
-
This project is maintained by the
|
|
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.
|
|
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
|
data/lib/zxcvbn/clock.rb
ADDED
data/lib/zxcvbn/crack_time.rb
CHANGED
|
@@ -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
|
|
11
|
+
0.5 * (2**entropy) * SECONDS_PER_GUESS
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def crack_time_to_score(seconds)
|
|
13
|
-
|
|
14
|
-
when seconds < 10**2
|
|
15
|
+
if seconds < 10**2
|
|
15
16
|
0
|
|
16
|
-
|
|
17
|
+
elsif seconds < 10**4
|
|
17
18
|
1
|
|
18
|
-
|
|
19
|
+
elsif seconds < 10**6
|
|
19
20
|
2
|
|
20
|
-
|
|
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
|
-
|
|
36
|
-
when seconds < minute
|
|
36
|
+
if seconds < minute
|
|
37
37
|
'instant'
|
|
38
|
-
|
|
38
|
+
elsif seconds < hour
|
|
39
39
|
"#{1 + (seconds / minute).ceil} minutes"
|
|
40
|
-
|
|
40
|
+
elsif seconds < day
|
|
41
41
|
"#{1 + (seconds / hour).ceil} hours"
|
|
42
|
-
|
|
42
|
+
elsif seconds < month
|
|
43
43
|
"#{1 + (seconds / day).ceil} days"
|
|
44
|
-
|
|
44
|
+
elsif seconds < year
|
|
45
45
|
"#{1 + (seconds / month).ceil} months"
|
|
46
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
5
|
-
|
|
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
|
|
11
|
-
|
|
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
|
data/lib/zxcvbn/entropy.rb
CHANGED
|
@@ -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 =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
37
|
-
1
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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(
|
|
61
|
+
def year_entropy(_match)
|
|
58
62
|
lg(NUM_YEARS)
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
def date_entropy(match)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
118
|
+
entropy.zero? ? 1 : entropy
|
|
115
119
|
end
|
|
116
120
|
|
|
117
121
|
def spatial_entropy(match)
|
|
118
|
-
if %w
|
|
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
|
|
138
|
+
possibilities += nCk(i - 1, j - 1) * starting_positions * average_degree**j
|
|
135
139
|
end
|
|
136
140
|
end
|
|
137
141
|
|
data/lib/zxcvbn/feedback.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zxcvbn
|
|
4
|
-
class Match
|
|
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
|
-
|
|
7
|
-
|
|
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
|
data/lib/zxcvbn/matchers/date.rb
CHANGED
|
@@ -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 |
|
|
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
|
-
|
|
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 | - |
|
|
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
|
-
|
|
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,
|
|
57
|
+
re_match_all(WITHOUT_SEPARATOR, password) do |match, _re_match|
|
|
56
58
|
extract_dates(match.token).each do |candidate|
|
|
57
|
-
day
|
|
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
|
-
:
|
|
75
|
-
:
|
|
76
|
-
:
|
|
78
|
+
year: +'',
|
|
79
|
+
month: +'',
|
|
80
|
+
day: +''
|
|
77
81
|
}
|
|
78
|
-
|
|
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[
|
|
96
|
-
7 => %w[
|
|
97
|
-
6 => %w[
|
|
98
|
-
5 => %w[
|
|
99
|
-
4 => %w[
|
|
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
|
-
|
|
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
|