zxcvbn-ruby 1.2.1 → 1.2.3
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 +23 -3
- data/README.md +2 -11
- data/lib/zxcvbn/clock.rb +2 -1
- data/lib/zxcvbn/crack_time.rb +14 -14
- data/lib/zxcvbn/data.rb +8 -6
- data/lib/zxcvbn/dictionary_ranker.rb +4 -2
- 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 +2 -0
- data/lib/zxcvbn/matchers/date.rb +26 -21
- data/lib/zxcvbn/matchers/dictionary.rb +14 -10
- data/lib/zxcvbn/matchers/digits.rb +3 -1
- data/lib/zxcvbn/matchers/l33t.rb +20 -16
- data/lib/zxcvbn/matchers/new_l33t.rb +27 -30
- 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 +6 -3
- data/lib/zxcvbn/omnimatch.rb +3 -1
- data/lib/zxcvbn/password_strength.rb +3 -1
- data/lib/zxcvbn/score.rb +3 -1
- data/lib/zxcvbn/scorer.rb +26 -23
- data/lib/zxcvbn/tester.rb +2 -2
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +4 -2
- metadata +2 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62e582cfff03a7e2075efea27771a6e8a542e799546b67ab7e342df77fc503e5
|
|
4
|
+
data.tar.gz: 9ac56545038db446e0dac7421f8d5ebc22bdb62abc808ba9801f18fbaf474257
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15a3f510c69b660169c08e7552f04ba5072db4865977aa436e3b673cd2053dadd0af5a5d143ea936a86993c1cbe48653957384bf97aac7056d80d31a99c986a9
|
|
7
|
+
data.tar.gz: 6c69b9142475710c4ce02ccbef7da7faaa4c54426118d29383f55d20c1b9c1d62459bd8de842f37830b22568ba41cb4a5717dc6531d9c4edb7625691cf3d1229
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,27 @@ 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.
|
|
9
|
+
[Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.3...HEAD
|
|
10
|
+
|
|
11
|
+
## [1.2.3] - 2025-12-07
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Address linting issues ([#52])
|
|
15
|
+
- Address style issues ([#53], [#54], [#55])
|
|
16
|
+
|
|
17
|
+
[1.2.3]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.2...v1.2.3
|
|
18
|
+
[#52]: https://github.com/envato/zxcvbn-ruby/pull/52
|
|
19
|
+
[#53]: https://github.com/envato/zxcvbn-ruby/pull/53
|
|
20
|
+
[#54]: https://github.com/envato/zxcvbn-ruby/pull/54
|
|
21
|
+
[#55]: https://github.com/envato/zxcvbn-ruby/pull/55
|
|
22
|
+
|
|
23
|
+
## [1.2.2] - 2025-12-06
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Address layout and frozen string literal issues ([#49])
|
|
27
|
+
|
|
28
|
+
[1.2.2]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.1...v1.2.2
|
|
29
|
+
[#49]: https://github.com/envato/zxcvbn-ruby/pull/49
|
|
10
30
|
|
|
11
31
|
## [1.2.1] - 2025-12-05
|
|
12
32
|
|
|
@@ -14,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
34
|
- Removed the dependency on the Ruby `benchmark` module ([#44]).
|
|
15
35
|
- Tests are no longer included in the gem package ([#45]).
|
|
16
36
|
|
|
17
|
-
[1.2.1]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...
|
|
37
|
+
[1.2.1]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...v1.2.1
|
|
18
38
|
[#44]: https://github.com/envato/zxcvbn-ruby/pull/44
|
|
19
39
|
[#45]: https://github.com/envato/zxcvbn-ruby/pull/45
|
|
20
40
|
|
|
@@ -27,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
27
47
|
- Use [mini\_racer] for running JavaScript specs (thanks [@RSO] ([#33]))
|
|
28
48
|
- Moved CI to GitHub Actions ([#34])
|
|
29
49
|
|
|
30
|
-
[1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...
|
|
50
|
+
[1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...v1.2.0
|
|
31
51
|
[@rso]: https://github.com/RSO
|
|
32
52
|
[mini\_racer]: https://rubygems.org/gems/mini_racer/
|
|
33
53
|
[#32]: https://github.com/envato/zxcvbn-ruby/pull/32
|
data/README.md
CHANGED
|
@@ -126,11 +126,6 @@ attacker. For this reason we advise you not to store the results of
|
|
|
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
CHANGED
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,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'json'
|
|
2
4
|
require 'zxcvbn/dictionary_ranker'
|
|
3
5
|
|
|
@@ -5,11 +7,11 @@ module Zxcvbn
|
|
|
5
7
|
class Data
|
|
6
8
|
def initialize
|
|
7
9
|
@ranked_dictionaries = DictionaryRanker.rank_dictionaries(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
'english' => read_word_list('english.txt'),
|
|
11
|
+
'female_names' => read_word_list('female_names.txt'),
|
|
12
|
+
'male_names' => read_word_list('male_names.txt'),
|
|
13
|
+
'passwords' => read_word_list('passwords.txt'),
|
|
14
|
+
'surnames' => read_word_list('surnames.txt')
|
|
13
15
|
)
|
|
14
16
|
@adjacency_graphs = JSON.load(DATA_PATH.join('adjacency_graphs.json').read)
|
|
15
17
|
end
|
|
@@ -23,7 +25,7 @@ module Zxcvbn
|
|
|
23
25
|
private
|
|
24
26
|
|
|
25
27
|
def read_word_list(file)
|
|
26
|
-
DATA_PATH.join(
|
|
28
|
+
DATA_PATH.join('frequency_lists', file).read.split
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
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
|
|
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
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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -18,19 +20,21 @@ module Zxcvbn
|
|
|
18
20
|
(0..password_length).each do |i|
|
|
19
21
|
(i...password_length).each do |j|
|
|
20
22
|
word = lowercased_password[i..j]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
next unless @ranked_dictionary.key?(word)
|
|
24
|
+
|
|
25
|
+
results << Match.new(
|
|
26
|
+
matched_word: word,
|
|
27
|
+
token: password[i..j],
|
|
28
|
+
i: i,
|
|
29
|
+
j: j,
|
|
30
|
+
rank: @ranked_dictionary[word],
|
|
31
|
+
pattern: 'dictionary',
|
|
32
|
+
dictionary_name: @name
|
|
33
|
+
)
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
results
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
|
-
end
|
|
40
|
+
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,7 +7,7 @@ module Zxcvbn
|
|
|
5
7
|
class Digits
|
|
6
8
|
include RegexHelpers
|
|
7
9
|
|
|
8
|
-
DIGITS_REGEX = /\d{3,}
|
|
10
|
+
DIGITS_REGEX = /\d{3,}/.freeze
|
|
9
11
|
|
|
10
12
|
def matches(password)
|
|
11
13
|
result = []
|
data/lib/zxcvbn/matchers/l33t.rb
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Zxcvbn
|
|
2
4
|
module Matchers
|
|
3
5
|
class L33t
|
|
4
6
|
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
|
-
}
|
|
7
|
+
'a' => ['4', '@'].freeze,
|
|
8
|
+
'b' => ['8'].freeze,
|
|
9
|
+
'c' => ['(', '{', '[', '<'].freeze,
|
|
10
|
+
'e' => ['3'].freeze,
|
|
11
|
+
'g' => ['6', '9'].freeze,
|
|
12
|
+
'i' => ['1', '!', '|'].freeze,
|
|
13
|
+
'l' => ['1', '|', '7'].freeze,
|
|
14
|
+
'o' => ['0'].freeze,
|
|
15
|
+
's' => ['$', '5'].freeze,
|
|
16
|
+
't' => ['+', '7'].freeze,
|
|
17
|
+
'x' => ['%'].freeze,
|
|
18
|
+
'z' => ['2'].freeze
|
|
19
|
+
}.freeze
|
|
18
20
|
|
|
19
21
|
def initialize(dictionary_matchers)
|
|
20
22
|
@dictionary_matchers = dictionary_matchers
|
|
@@ -30,6 +32,7 @@ module Zxcvbn
|
|
|
30
32
|
matcher.matches(subbed_password).each do |match|
|
|
31
33
|
token = password[match.i..match.j]
|
|
32
34
|
next if token.downcase == match.matched_word.downcase
|
|
35
|
+
|
|
33
36
|
match_substitutions = {}
|
|
34
37
|
substitution.each do |s, letter|
|
|
35
38
|
match_substitutions[s] = letter if token.include?(s)
|
|
@@ -79,6 +82,7 @@ module Zxcvbn
|
|
|
79
82
|
|
|
80
83
|
def find_substitutions(subs, table, keys)
|
|
81
84
|
return subs if keys.empty?
|
|
85
|
+
|
|
82
86
|
first_key = keys[0]
|
|
83
87
|
rest_keys = keys[1..-1]
|
|
84
88
|
next_subs = []
|
|
@@ -113,8 +117,8 @@ module Zxcvbn
|
|
|
113
117
|
subs.each do |sub|
|
|
114
118
|
assoc = sub.dup
|
|
115
119
|
|
|
116
|
-
assoc.sort!
|
|
117
|
-
label = assoc.map{|k, v| "#{k},#{v}"}.join('-')
|
|
120
|
+
assoc.sort!
|
|
121
|
+
label = assoc.map { |k, v| "#{k},#{v}" }.join('-')
|
|
118
122
|
unless members.include?(label)
|
|
119
123
|
members << label
|
|
120
124
|
deduped << sub
|
|
@@ -124,4 +128,4 @@ module Zxcvbn
|
|
|
124
128
|
end
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
|
-
end
|
|
131
|
+
end
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Zxcvbn
|
|
2
4
|
module Matchers
|
|
3
5
|
class L33t
|
|
4
6
|
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
|
-
}
|
|
7
|
+
'a' => ['4', '@'].freeze,
|
|
8
|
+
'b' => ['8'].freeze,
|
|
9
|
+
'c' => ['(', '{', '[', '<'].freeze,
|
|
10
|
+
'e' => ['3'].freeze,
|
|
11
|
+
'g' => ['6', '9'].freeze,
|
|
12
|
+
'i' => ['1', '!', '|'].freeze,
|
|
13
|
+
'l' => ['1', '|', '7'].freeze,
|
|
14
|
+
'o' => ['0'].freeze,
|
|
15
|
+
's' => ['$', '5'].freeze,
|
|
16
|
+
't' => ['+', '7'].freeze,
|
|
17
|
+
'x' => ['%'].freeze,
|
|
18
|
+
'z' => ['2'].freeze
|
|
19
|
+
}.freeze
|
|
18
20
|
|
|
19
21
|
def initialize(dictionary_matchers)
|
|
20
22
|
@dictionary_matchers = dictionary_matchers
|
|
@@ -24,16 +26,15 @@ module Zxcvbn
|
|
|
24
26
|
matches = []
|
|
25
27
|
lowercased_password = password.downcase
|
|
26
28
|
combinations_to_try = substitution_combinations(relevant_l33t_substitutions(lowercased_password))
|
|
27
|
-
|
|
28
|
-
combinations_to_try.each do |substitution|
|
|
29
|
+
combinations_to_try.each do |substitutions|
|
|
29
30
|
@dictionary_matchers.each do |matcher|
|
|
30
|
-
subbed_password = substitute(lowercased_password,
|
|
31
|
+
subbed_password = substitute(lowercased_password, substitutions)
|
|
31
32
|
matcher.matches(subbed_password).each do |match|
|
|
32
33
|
token = lowercased_password[match.i..match.j]
|
|
33
34
|
next if token == match.matched_word.downcase
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
match_substitutions = {}
|
|
36
|
-
|
|
37
|
+
substitutions.each do |letter, substitution|
|
|
37
38
|
match_substitutions[substitution] = letter if token.include?(substitution)
|
|
38
39
|
end
|
|
39
40
|
match.l33t = true
|
|
@@ -49,9 +50,9 @@ module Zxcvbn
|
|
|
49
50
|
matches
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
def substitute(password,
|
|
53
|
+
def substitute(password, substitutions)
|
|
53
54
|
subbed_password = password.dup
|
|
54
|
-
|
|
55
|
+
substitutions.each do |letter, substitution|
|
|
55
56
|
subbed_password.gsub!(substitution, letter)
|
|
56
57
|
end
|
|
57
58
|
subbed_password
|
|
@@ -64,9 +65,7 @@ module Zxcvbn
|
|
|
64
65
|
end
|
|
65
66
|
L33T_TABLE.each do |letter, substibutions|
|
|
66
67
|
password.each_char do |password_char|
|
|
67
|
-
if substibutions.include?(password_char)
|
|
68
|
-
subs[letter] << password_char
|
|
69
|
-
end
|
|
68
|
+
subs[letter] << password_char if substibutions.include?(password_char)
|
|
70
69
|
end
|
|
71
70
|
end
|
|
72
71
|
subs
|
|
@@ -82,7 +81,7 @@ module Zxcvbn
|
|
|
82
81
|
expanded_substitutions.each do |substitution_hash|
|
|
83
82
|
# convert a hash to an array of hashes with 1 key each
|
|
84
83
|
subs_array = substitution_hash.map do |letter, substitutions|
|
|
85
|
-
{letter => substitutions}
|
|
84
|
+
{ letter => substitutions }
|
|
86
85
|
end
|
|
87
86
|
combinations << subs_array
|
|
88
87
|
|
|
@@ -94,15 +93,13 @@ module Zxcvbn
|
|
|
94
93
|
end
|
|
95
94
|
|
|
96
95
|
# convert back to simple hash per substitution combination
|
|
97
|
-
|
|
96
|
+
combinations.map do |combination_set|
|
|
98
97
|
hash = {}
|
|
99
98
|
combination_set.each do |combination_hash|
|
|
100
99
|
hash.merge!(combination_hash)
|
|
101
100
|
end
|
|
102
101
|
hash
|
|
103
102
|
end
|
|
104
|
-
|
|
105
|
-
combination_hashes
|
|
106
103
|
end
|
|
107
104
|
|
|
108
105
|
# expand possible combinations if multiple characters can be substituted
|
|
@@ -110,11 +107,11 @@ module Zxcvbn
|
|
|
110
107
|
# [{'a' => '4', 'i' => 1}, {'a' => '@', 'i' => '1'}]
|
|
111
108
|
def expanded_substitutions(hash)
|
|
112
109
|
return {} if hash.empty?
|
|
110
|
+
|
|
113
111
|
values = hash.values
|
|
114
112
|
product_values = values[0].product(*values[1..-1])
|
|
115
|
-
product_values.map{ |p| Hash[hash.keys.zip(p)] }
|
|
113
|
+
product_values.map { |p| Hash[hash.keys.zip(p)] }
|
|
116
114
|
end
|
|
117
|
-
|
|
118
115
|
end
|
|
119
116
|
end
|
|
120
|
-
end
|
|
117
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -5,15 +7,15 @@ module Zxcvbn
|
|
|
5
7
|
module RegexHelpers
|
|
6
8
|
def re_match_all(regex, password)
|
|
7
9
|
pos = 0
|
|
8
|
-
while re_match = regex.match(password, pos)
|
|
10
|
+
while (re_match = regex.match(password, pos))
|
|
9
11
|
i, j = re_match.offset(0)
|
|
10
12
|
pos = j
|
|
11
13
|
j -= 1
|
|
12
14
|
|
|
13
15
|
match = Match.new(
|
|
14
|
-
:
|
|
15
|
-
:
|
|
16
|
-
:
|
|
16
|
+
i: i,
|
|
17
|
+
j: j,
|
|
18
|
+
token: password[i..j]
|
|
17
19
|
)
|
|
18
20
|
yield match, re_match
|
|
19
21
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -9,17 +11,15 @@ module Zxcvbn
|
|
|
9
11
|
while i < password.length
|
|
10
12
|
cur_char = password[i]
|
|
11
13
|
j = i + 1
|
|
12
|
-
while cur_char == password[j]
|
|
13
|
-
j += 1
|
|
14
|
-
end
|
|
14
|
+
j += 1 while cur_char == password[j]
|
|
15
15
|
|
|
16
16
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
17
17
|
result << Match.new(
|
|
18
|
-
:
|
|
19
|
-
:
|
|
20
|
-
:j
|
|
21
|
-
:
|
|
22
|
-
:
|
|
18
|
+
pattern: 'repeat',
|
|
19
|
+
i: i,
|
|
20
|
+
j: j - 1,
|
|
21
|
+
token: password[i...j],
|
|
22
|
+
repeated_char: cur_char
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -7,14 +9,14 @@ module Zxcvbn
|
|
|
7
9
|
'lower' => 'abcdefghijklmnopqrstuvwxyz',
|
|
8
10
|
'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
9
11
|
'digits' => '01234567890'
|
|
10
|
-
}
|
|
12
|
+
}.freeze
|
|
11
13
|
|
|
12
14
|
def seq_match_length(password, from, direction, seq)
|
|
13
15
|
index_from = seq.index(password[from])
|
|
14
16
|
j = 1
|
|
15
17
|
while from + j < password.length &&
|
|
16
18
|
password[from + j] == seq[index_from + direction * j]
|
|
17
|
-
j+= 1
|
|
19
|
+
j += 1
|
|
18
20
|
end
|
|
19
21
|
j
|
|
20
22
|
end
|
|
@@ -24,15 +26,13 @@ module Zxcvbn
|
|
|
24
26
|
def applicable_sequence(password, i)
|
|
25
27
|
SEQUENCES.each do |name, sequence|
|
|
26
28
|
index1 = sequence.index(password[i])
|
|
27
|
-
index2 = sequence.index(password[i+1])
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
end
|
|
29
|
+
index2 = sequence.index(password[i + 1])
|
|
30
|
+
next unless index1 && index2
|
|
31
|
+
|
|
32
|
+
seq_direction = index2 - index1
|
|
33
|
+
return [name, sequence, seq_direction] if [-1, 1].include?(seq_direction)
|
|
34
|
+
|
|
35
|
+
return nil
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -46,13 +46,13 @@ module Zxcvbn
|
|
|
46
46
|
length = seq_match_length(password, i, seq_direction, seq)
|
|
47
47
|
if length > 2
|
|
48
48
|
result << Match.new(
|
|
49
|
-
:
|
|
50
|
-
:
|
|
51
|
-
:
|
|
52
|
-
:
|
|
53
|
-
:
|
|
54
|
-
:
|
|
55
|
-
:
|
|
49
|
+
pattern: 'sequence',
|
|
50
|
+
i: i,
|
|
51
|
+
j: i + length - 1,
|
|
52
|
+
token: password[i, length],
|
|
53
|
+
sequence_name: seq_name,
|
|
54
|
+
sequence_space: seq.length,
|
|
55
|
+
ascending: seq_direction == 1
|
|
56
56
|
)
|
|
57
57
|
end
|
|
58
58
|
i += length - 1
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/match'
|
|
2
4
|
|
|
3
5
|
module Zxcvbn
|
|
@@ -24,7 +26,7 @@ module Zxcvbn
|
|
|
24
26
|
turns = 0
|
|
25
27
|
shifted_count = 0
|
|
26
28
|
loop do
|
|
27
|
-
prev_char = password[j-1]
|
|
29
|
+
prev_char = password[j - 1]
|
|
28
30
|
found = false
|
|
29
31
|
found_direction = -1
|
|
30
32
|
cur_direction = -1
|
|
@@ -34,22 +36,22 @@ module Zxcvbn
|
|
|
34
36
|
cur_char = password[j]
|
|
35
37
|
adjacents.each do |adj|
|
|
36
38
|
cur_direction += 1
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
break
|
|
39
|
+
next unless adj&.index(cur_char)
|
|
40
|
+
|
|
41
|
+
found = true
|
|
42
|
+
found_direction = cur_direction
|
|
43
|
+
if adj.index(cur_char) == 1
|
|
44
|
+
# index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
|
|
45
|
+
# for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
|
|
46
|
+
shifted_count += 1
|
|
47
|
+
end
|
|
48
|
+
if last_direction != found_direction
|
|
49
|
+
# adding a turn is correct even in the initial case when last_direction is null:
|
|
50
|
+
# every spatial pattern starts with a turn.
|
|
51
|
+
turns += 1
|
|
52
|
+
last_direction = found_direction
|
|
52
53
|
end
|
|
54
|
+
break
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
# if the current pattern continued, extend j and try to grow again
|
|
@@ -59,13 +61,13 @@ module Zxcvbn
|
|
|
59
61
|
# otherwise push the pattern discovered so far, if any...
|
|
60
62
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
61
63
|
result << Match.new(
|
|
62
|
-
:
|
|
63
|
-
:
|
|
64
|
-
:j
|
|
65
|
-
:
|
|
66
|
-
:
|
|
67
|
-
:
|
|
68
|
-
:
|
|
64
|
+
pattern: 'spatial',
|
|
65
|
+
i: i,
|
|
66
|
+
j: j - 1,
|
|
67
|
+
token: password[i...j],
|
|
68
|
+
graph: graph_name,
|
|
69
|
+
turns: turns,
|
|
70
|
+
shifted_count: shifted_count
|
|
69
71
|
)
|
|
70
72
|
end
|
|
71
73
|
# ...and then start a new search for the rest of the password.
|
|
@@ -78,4 +80,4 @@ module Zxcvbn
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
|
-
end
|
|
83
|
+
end
|
data/lib/zxcvbn/matchers/year.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,7 +7,7 @@ module Zxcvbn
|
|
|
5
7
|
class Year
|
|
6
8
|
include RegexHelpers
|
|
7
9
|
|
|
8
|
-
YEAR_REGEX = /19\d\d|200\d|201\d
|
|
10
|
+
YEAR_REGEX = /19\d\d|200\d|201\d/.freeze
|
|
9
11
|
|
|
10
12
|
def matches(password)
|
|
11
13
|
result = []
|
|
@@ -17,4 +19,4 @@ module Zxcvbn
|
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
|
-
end
|
|
22
|
+
end
|
data/lib/zxcvbn/math.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Zxcvbn
|
|
2
4
|
module Math
|
|
3
5
|
def bruteforce_cardinality(password)
|
|
@@ -30,11 +32,12 @@ module Zxcvbn
|
|
|
30
32
|
|
|
31
33
|
def nCk(n, k)
|
|
32
34
|
return 0 if k > n
|
|
33
|
-
return 1 if k
|
|
35
|
+
return 1 if k.zero?
|
|
36
|
+
|
|
34
37
|
r = 1
|
|
35
38
|
(1..k).each do |d|
|
|
36
|
-
r
|
|
37
|
-
r
|
|
39
|
+
r *= n
|
|
40
|
+
r /= d
|
|
38
41
|
n -= 1
|
|
39
42
|
end
|
|
40
43
|
r
|
data/lib/zxcvbn/omnimatch.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/dictionary_ranker'
|
|
2
4
|
require 'zxcvbn/matchers/dictionary'
|
|
3
5
|
require 'zxcvbn/matchers/l33t'
|
|
@@ -26,13 +28,13 @@ module Zxcvbn
|
|
|
26
28
|
|
|
27
29
|
def user_input_matchers(user_inputs)
|
|
28
30
|
return [] unless user_inputs.any?
|
|
31
|
+
|
|
29
32
|
user_ranked_dictionary = DictionaryRanker.rank_dictionary(user_inputs)
|
|
30
33
|
dictionary_matcher = Matchers::Dictionary.new('user_inputs', user_ranked_dictionary)
|
|
31
34
|
l33t_matcher = Matchers::L33t.new([dictionary_matcher])
|
|
32
35
|
[dictionary_matcher, l33t_matcher]
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
|
|
36
38
|
def build_matchers
|
|
37
39
|
matchers = []
|
|
38
40
|
dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/clock'
|
|
2
4
|
require 'zxcvbn/feedback_giver'
|
|
3
5
|
require 'zxcvbn/omnimatch'
|
|
@@ -11,7 +13,7 @@ module Zxcvbn
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def test(password, user_inputs = [])
|
|
14
|
-
password
|
|
16
|
+
password ||= ''
|
|
15
17
|
result = nil
|
|
16
18
|
calc_time = Clock.realtime do
|
|
17
19
|
matches = @omnimatch.matches(password, user_inputs)
|
data/lib/zxcvbn/score.rb
CHANGED
data/lib/zxcvbn/scorer.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zxcvbn/entropy'
|
|
2
4
|
require 'zxcvbn/crack_time'
|
|
3
5
|
require 'zxcvbn/score'
|
|
@@ -16,19 +18,22 @@ module Zxcvbn
|
|
|
16
18
|
|
|
17
19
|
def minimum_entropy_match_sequence(password, matches)
|
|
18
20
|
bruteforce_cardinality = bruteforce_cardinality(password) # e.g. 26 for lowercase
|
|
19
|
-
up_to_k = []
|
|
20
|
-
|
|
21
|
+
up_to_k = [] # minimum entropy up to k.
|
|
22
|
+
# for the optimal sequence of matches up to k, holds the final match (match.j == k).
|
|
23
|
+
# null means the sequence ends w/ a brute-force character.
|
|
24
|
+
backpointers = []
|
|
21
25
|
(0...password.length).each do |k|
|
|
22
26
|
# starting scenario to try and beat: adding a brute-force character to the minimum entropy sequence at k-1.
|
|
23
|
-
previous_k_entropy = k
|
|
27
|
+
previous_k_entropy = k.positive? ? up_to_k[k - 1] : 0
|
|
24
28
|
up_to_k[k] = previous_k_entropy + lg(bruteforce_cardinality)
|
|
25
29
|
backpointers[k] = nil
|
|
26
30
|
matches.select do |match|
|
|
27
31
|
match.j == k
|
|
28
32
|
end.each do |match|
|
|
29
|
-
i
|
|
33
|
+
i = match.i
|
|
34
|
+
j = match.j
|
|
30
35
|
# see if best entropy up to i-1 + entropy of this match is less than the current minimum at j.
|
|
31
|
-
previous_i_entropy = i
|
|
36
|
+
previous_i_entropy = i.positive? ? up_to_k[i - 1] : 0
|
|
32
37
|
candidate_entropy = previous_i_entropy + calc_entropy(match)
|
|
33
38
|
if up_to_k[j] && candidate_entropy < up_to_k[j]
|
|
34
39
|
up_to_k[j] = candidate_entropy
|
|
@@ -54,29 +59,26 @@ module Zxcvbn
|
|
|
54
59
|
score_for(password, match_sequence, up_to_k)
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
def score_for
|
|
58
|
-
min_entropy = up_to_k[password.length - 1] || 0
|
|
62
|
+
def score_for(password, match_sequence, up_to_k)
|
|
63
|
+
min_entropy = up_to_k[password.length - 1] || 0 # or 0 corner case is for an empty password ''
|
|
59
64
|
crack_time = entropy_to_crack_time(min_entropy)
|
|
60
65
|
|
|
61
66
|
# final result object
|
|
62
67
|
Score.new(
|
|
63
|
-
:
|
|
64
|
-
:
|
|
65
|
-
:
|
|
66
|
-
:
|
|
67
|
-
:
|
|
68
|
-
:
|
|
68
|
+
password: password,
|
|
69
|
+
entropy: min_entropy.round(3),
|
|
70
|
+
match_sequence: match_sequence,
|
|
71
|
+
crack_time: crack_time.round(3),
|
|
72
|
+
crack_time_display: display_time(crack_time),
|
|
73
|
+
score: crack_time_to_score(crack_time)
|
|
69
74
|
)
|
|
70
75
|
end
|
|
71
76
|
|
|
72
|
-
|
|
73
77
|
def pad_with_bruteforce_matches(match_sequence, password, bruteforce_cardinality)
|
|
74
78
|
k = 0
|
|
75
79
|
match_sequence_copy = []
|
|
76
80
|
match_sequence.each do |match|
|
|
77
|
-
if match.i > k
|
|
78
|
-
match_sequence_copy << make_bruteforce_match(password, k, match.i - 1, bruteforce_cardinality)
|
|
79
|
-
end
|
|
81
|
+
match_sequence_copy << make_bruteforce_match(password, k, match.i - 1, bruteforce_cardinality) if match.i > k
|
|
80
82
|
k = match.j + 1
|
|
81
83
|
match_sequence_copy << match
|
|
82
84
|
end
|
|
@@ -85,17 +87,18 @@ module Zxcvbn
|
|
|
85
87
|
end
|
|
86
88
|
match_sequence_copy
|
|
87
89
|
end
|
|
90
|
+
|
|
88
91
|
# fill in the blanks between pattern matches with bruteforce "matches"
|
|
89
92
|
# that way the match sequence fully covers the password:
|
|
90
93
|
# match1.j == match2.i - 1 for every adjacent match1, match2.
|
|
91
94
|
def make_bruteforce_match(password, i, j, bruteforce_cardinality)
|
|
92
95
|
Match.new(
|
|
93
|
-
:
|
|
94
|
-
:
|
|
95
|
-
:
|
|
96
|
-
:
|
|
97
|
-
:
|
|
98
|
-
:
|
|
96
|
+
pattern: 'bruteforce',
|
|
97
|
+
i: i,
|
|
98
|
+
j: j,
|
|
99
|
+
token: password[i..j],
|
|
100
|
+
entropy: lg(bruteforce_cardinality**(j - i + 1)),
|
|
101
|
+
cardinality: bruteforce_cardinality
|
|
99
102
|
)
|
|
100
103
|
end
|
|
101
104
|
end
|
data/lib/zxcvbn/tester.rb
CHANGED
data/lib/zxcvbn/version.rb
CHANGED
data/lib/zxcvbn.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'pathname'
|
|
2
4
|
require 'zxcvbn/version'
|
|
3
5
|
require 'zxcvbn/tester'
|
|
4
6
|
|
|
5
7
|
module Zxcvbn
|
|
6
|
-
|
|
8
|
+
module_function
|
|
7
9
|
|
|
8
|
-
DATA_PATH = Pathname(File.expand_path('
|
|
10
|
+
DATA_PATH = Pathname(File.expand_path('../data', __dir__))
|
|
9
11
|
|
|
10
12
|
# Returns a Zxcvbn::Score for the given password
|
|
11
13
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zxcvbn-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Hodgkiss
|
|
@@ -9,35 +9,7 @@ authors:
|
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
-
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: mini_racer
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: rspec
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
12
|
+
dependencies: []
|
|
41
13
|
description: Ruby port of Dropboxs zxcvbn.js
|
|
42
14
|
email:
|
|
43
15
|
- steve@hodgkiss.me
|