zxcvbn-ruby 1.2.2 → 1.2.4
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 +24 -4
- data/README.md +2 -11
- data/lib/zxcvbn/crack_time.rb +10 -12
- data/lib/zxcvbn/data.rb +7 -7
- data/lib/zxcvbn/dictionary_ranker.rb +2 -2
- data/lib/zxcvbn/entropy.rb +14 -15
- data/lib/zxcvbn/feedback_giver.rb +2 -3
- data/lib/zxcvbn/matchers/date.rb +23 -21
- data/lib/zxcvbn/matchers/dictionary.rb +11 -9
- data/lib/zxcvbn/matchers/digits.rb +1 -1
- data/lib/zxcvbn/matchers/l33t.rb +14 -14
- data/lib/zxcvbn/matchers/new_l33t.rb +20 -26
- data/lib/zxcvbn/matchers/regex_helpers.rb +4 -4
- data/lib/zxcvbn/matchers/repeat.rb +6 -8
- data/lib/zxcvbn/matchers/sequences.rb +14 -16
- data/lib/zxcvbn/matchers/spatial.rb +22 -22
- data/lib/zxcvbn/matchers/year.rb +1 -1
- data/lib/zxcvbn/math.rb +3 -3
- data/lib/zxcvbn/password_strength.rb +1 -1
- data/lib/zxcvbn/scorer.rb +18 -19
- data/lib/zxcvbn/tester.rb +2 -2
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52ddbaaabcb2929e59d34d91104bce81b86f223428001218872294363d79f2ac
|
|
4
|
+
data.tar.gz: 5c488ab8d0dfbd6c46b2b5b05f3b878355e09babe09304c3653c9e45dcf64d02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ac904b4f3e4219981def358d1f7aa69870e9e33605c3696123e95f936d4880b39d2b5e1d5f323fd89ebc7b2ff43eaa93f22c598ac40ea20bdd768072a082162
|
|
7
|
+
data.tar.gz: f32d688a3ee53867c1f47f0e52527d3da4397e26c93854305ee5cbcf224e9dad104f087ab80174848b3765dd41a086a6fd33a4ab72a1b72d8ea05209aa88b289
|
data/CHANGELOG.md
CHANGED
|
@@ -6,14 +6,34 @@ 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.4...HEAD
|
|
10
|
+
|
|
11
|
+
## [1.2.4] - 2025-12-07
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Address security issues found by RuboCop ([#57])
|
|
15
|
+
|
|
16
|
+
[1.2.4]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.3...v1.2.4
|
|
17
|
+
[#57]: https://github.com/envato/zxcvbn-ruby/pull/57
|
|
18
|
+
|
|
19
|
+
## [1.2.3] - 2025-12-07
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Address linting issues found by RuboCop ([#52])
|
|
23
|
+
- Address style issues found by RuboCop ([#53], [#54], [#55])
|
|
24
|
+
|
|
25
|
+
[1.2.3]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.2...v1.2.3
|
|
26
|
+
[#52]: https://github.com/envato/zxcvbn-ruby/pull/52
|
|
27
|
+
[#53]: https://github.com/envato/zxcvbn-ruby/pull/53
|
|
28
|
+
[#54]: https://github.com/envato/zxcvbn-ruby/pull/54
|
|
29
|
+
[#55]: https://github.com/envato/zxcvbn-ruby/pull/55
|
|
10
30
|
|
|
11
31
|
## [1.2.2] - 2025-12-06
|
|
12
32
|
|
|
13
33
|
### Changed
|
|
14
34
|
- Address layout and frozen string literal issues ([#49])
|
|
15
35
|
|
|
16
|
-
[1.2.2]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.1...
|
|
36
|
+
[1.2.2]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.1...v1.2.2
|
|
17
37
|
[#49]: https://github.com/envato/zxcvbn-ruby/pull/49
|
|
18
38
|
|
|
19
39
|
## [1.2.1] - 2025-12-05
|
|
@@ -22,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
22
42
|
- Removed the dependency on the Ruby `benchmark` module ([#44]).
|
|
23
43
|
- Tests are no longer included in the gem package ([#45]).
|
|
24
44
|
|
|
25
|
-
[1.2.1]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...
|
|
45
|
+
[1.2.1]: https://github.com/envato/zxcvbn-ruby/compare/v1.2.0...v1.2.1
|
|
26
46
|
[#44]: https://github.com/envato/zxcvbn-ruby/pull/44
|
|
27
47
|
[#45]: https://github.com/envato/zxcvbn-ruby/pull/45
|
|
28
48
|
|
|
@@ -35,7 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
35
55
|
- Use [mini\_racer] for running JavaScript specs (thanks [@RSO] ([#33]))
|
|
36
56
|
- Moved CI to GitHub Actions ([#34])
|
|
37
57
|
|
|
38
|
-
[1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...
|
|
58
|
+
[1.2.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.1.0...v1.2.0
|
|
39
59
|
[@rso]: https://github.com/RSO
|
|
40
60
|
[mini\_racer]: https://rubygems.org/gems/mini_racer/
|
|
41
61
|
[#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/crack_time.rb
CHANGED
|
@@ -12,14 +12,13 @@ module Zxcvbn
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def crack_time_to_score(seconds)
|
|
15
|
-
|
|
16
|
-
when seconds < 10**2
|
|
15
|
+
if seconds < 10**2
|
|
17
16
|
0
|
|
18
|
-
|
|
17
|
+
elsif seconds < 10**4
|
|
19
18
|
1
|
|
20
|
-
|
|
19
|
+
elsif seconds < 10**6
|
|
21
20
|
2
|
|
22
|
-
|
|
21
|
+
elsif seconds < 10**8
|
|
23
22
|
3
|
|
24
23
|
else
|
|
25
24
|
4
|
|
@@ -34,18 +33,17 @@ module Zxcvbn
|
|
|
34
33
|
year = month * 12
|
|
35
34
|
century = year * 100
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
when seconds < minute
|
|
36
|
+
if seconds < minute
|
|
39
37
|
'instant'
|
|
40
|
-
|
|
38
|
+
elsif seconds < hour
|
|
41
39
|
"#{1 + (seconds / minute).ceil} minutes"
|
|
42
|
-
|
|
40
|
+
elsif seconds < day
|
|
43
41
|
"#{1 + (seconds / hour).ceil} hours"
|
|
44
|
-
|
|
42
|
+
elsif seconds < month
|
|
45
43
|
"#{1 + (seconds / day).ceil} days"
|
|
46
|
-
|
|
44
|
+
elsif seconds < year
|
|
47
45
|
"#{1 + (seconds / month).ceil} months"
|
|
48
|
-
|
|
46
|
+
elsif seconds < century
|
|
49
47
|
"#{1 + (seconds / year).ceil} years"
|
|
50
48
|
else
|
|
51
49
|
'centuries'
|
data/lib/zxcvbn/data.rb
CHANGED
|
@@ -7,13 +7,13 @@ module Zxcvbn
|
|
|
7
7
|
class Data
|
|
8
8
|
def initialize
|
|
9
9
|
@ranked_dictionaries = DictionaryRanker.rank_dictionaries(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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')
|
|
15
15
|
)
|
|
16
|
-
@adjacency_graphs = JSON.
|
|
16
|
+
@adjacency_graphs = JSON.parse(DATA_PATH.join('adjacency_graphs.json').read)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
attr_reader :ranked_dictionaries, :adjacency_graphs
|
|
@@ -25,7 +25,7 @@ module Zxcvbn
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
def read_word_list(file)
|
|
28
|
-
DATA_PATH.join(
|
|
28
|
+
DATA_PATH.join('frequency_lists', file).read.split
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Zxcvbn
|
|
4
4
|
class DictionaryRanker
|
|
5
5
|
def self.rank_dictionaries(lists)
|
|
6
|
-
lists.
|
|
7
|
-
|
|
6
|
+
lists.transform_values do |words|
|
|
7
|
+
rank_dictionary(words)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
data/lib/zxcvbn/entropy.rb
CHANGED
|
@@ -58,20 +58,19 @@ module Zxcvbn::Entropy
|
|
|
58
58
|
NUM_MONTHS = 12
|
|
59
59
|
NUM_DAYS = 31
|
|
60
60
|
|
|
61
|
-
def year_entropy(
|
|
61
|
+
def year_entropy(_match)
|
|
62
62
|
lg(NUM_YEARS)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def date_entropy(match)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
71
72
|
|
|
72
|
-
if match.separator
|
|
73
|
-
entropy += 2
|
|
74
|
-
end
|
|
73
|
+
entropy += 2 if match.separator
|
|
75
74
|
|
|
76
75
|
entropy
|
|
77
76
|
end
|
|
@@ -84,10 +83,10 @@ module Zxcvbn::Entropy
|
|
|
84
83
|
match.base_entropy + match.uppercase_entropy + match.l33t_entropy
|
|
85
84
|
end
|
|
86
85
|
|
|
87
|
-
START_UPPER = /^[A-Z][^A-Z]
|
|
88
|
-
END_UPPER = /^[^A-Z]+[A-Z]
|
|
89
|
-
ALL_UPPER = /^[A-Z]
|
|
90
|
-
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
|
|
91
90
|
|
|
92
91
|
def extra_uppercase_entropy(match)
|
|
93
92
|
word = match.token
|
|
@@ -116,11 +115,11 @@ module Zxcvbn::Entropy
|
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
117
|
entropy = lg(possibilities)
|
|
119
|
-
entropy
|
|
118
|
+
entropy.zero? ? 1 : entropy
|
|
120
119
|
end
|
|
121
120
|
|
|
122
121
|
def spatial_entropy(match)
|
|
123
|
-
if %w
|
|
122
|
+
if %w[qwerty dvorak].include? match.graph
|
|
124
123
|
starting_positions = starting_positions_for_graph('qwerty')
|
|
125
124
|
average_degree = average_degree_for_graph('qwerty')
|
|
126
125
|
else
|
|
@@ -18,14 +18,14 @@ module Zxcvbn
|
|
|
18
18
|
|
|
19
19
|
def self.get_feedback(score, sequence)
|
|
20
20
|
# starting feedback
|
|
21
|
-
return DEFAULT_FEEDBACK if sequence.
|
|
21
|
+
return DEFAULT_FEEDBACK if sequence.empty?
|
|
22
22
|
|
|
23
23
|
# no feedback if score is good or great.
|
|
24
24
|
return EMPTY_FEEDBACK if score > 2
|
|
25
25
|
|
|
26
26
|
# tie feedback to the longest match for longer sequences
|
|
27
27
|
longest_match = sequence[0]
|
|
28
|
-
|
|
28
|
+
sequence[1..-1].each do |match|
|
|
29
29
|
longest_match = match if match.token.length > longest_match.token.length
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -47,7 +47,6 @@ module Zxcvbn
|
|
|
47
47
|
get_dictionary_match_feedback match, is_sole_match
|
|
48
48
|
|
|
49
49
|
when 'spatial'
|
|
50
|
-
layout = match.graph.upcase
|
|
51
50
|
warning = if match.turns == 1
|
|
52
51
|
'Straight rows of keys are easy to guess'
|
|
53
52
|
else
|
data/lib/zxcvbn/matchers/date.rb
CHANGED
|
@@ -7,23 +7,23 @@ module Zxcvbn
|
|
|
7
7
|
class Date
|
|
8
8
|
include RegexHelpers
|
|
9
9
|
|
|
10
|
-
YEAR_SUFFIX =
|
|
10
|
+
YEAR_SUFFIX = %r{
|
|
11
11
|
( \d{1,2} ) # day or month
|
|
12
|
-
( \s |
|
|
12
|
+
( \s | - | / | \\ | _ | \. ) # separator
|
|
13
13
|
( \d{1,2} ) # month or day
|
|
14
14
|
\2 # same separator
|
|
15
15
|
( 19\d{2} | 200\d | 201\d | \d{2} ) # year
|
|
16
|
-
|
|
16
|
+
}x.freeze
|
|
17
17
|
|
|
18
|
-
YEAR_PREFIX =
|
|
18
|
+
YEAR_PREFIX = %r{
|
|
19
19
|
( 19\d{2} | 200\d | 201\d | \d{2} ) # year
|
|
20
|
-
( \s | - |
|
|
20
|
+
( \s | - | / | \\ | _ | \. ) # separator
|
|
21
21
|
( \d{1,2} ) # day or month
|
|
22
22
|
\2 # same separator
|
|
23
23
|
( \d{1,2} ) # month or day
|
|
24
|
-
|
|
24
|
+
}x.freeze
|
|
25
25
|
|
|
26
|
-
WITHOUT_SEPARATOR = /\d{4,8}
|
|
26
|
+
WITHOUT_SEPARATOR = /\d{4,8}/.freeze
|
|
27
27
|
|
|
28
28
|
def matches(password)
|
|
29
29
|
match_with_separator(password) + match_without_separator(password)
|
|
@@ -54,9 +54,11 @@ module Zxcvbn
|
|
|
54
54
|
|
|
55
55
|
def match_without_separator(password)
|
|
56
56
|
result = []
|
|
57
|
-
re_match_all(WITHOUT_SEPARATOR, password) do |match,
|
|
57
|
+
re_match_all(WITHOUT_SEPARATOR, password) do |match, _re_match|
|
|
58
58
|
extract_dates(match.token).each do |candidate|
|
|
59
|
-
day
|
|
59
|
+
day = candidate[:day]
|
|
60
|
+
month = candidate[:month]
|
|
61
|
+
year = candidate[:year]
|
|
60
62
|
|
|
61
63
|
match.pattern = 'date'
|
|
62
64
|
match.day = day
|
|
@@ -73,11 +75,11 @@ module Zxcvbn
|
|
|
73
75
|
dates = []
|
|
74
76
|
date_patterns_for_length(token.length).map do |pattern|
|
|
75
77
|
candidate = {
|
|
76
|
-
:
|
|
77
|
-
:
|
|
78
|
-
:
|
|
78
|
+
year: +'',
|
|
79
|
+
month: +'',
|
|
80
|
+
day: +''
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
(0...token.length).each do |i|
|
|
81
83
|
candidate[PATTERN_CHAR_TO_SYM[pattern[i]]] << token[i]
|
|
82
84
|
end
|
|
83
85
|
candidate.each do |component, value|
|
|
@@ -94,18 +96,18 @@ module Zxcvbn
|
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
DATE_PATTERN_FOR_LENGTH = {
|
|
97
|
-
8 => %w[yyyymmdd ddmmyyyy mmddyyyy],
|
|
98
|
-
7 => %w[yyyymdd yyyymmd ddmyyyy dmmyyyy],
|
|
99
|
-
6 => %w[yymmdd ddmmyy mmddyy],
|
|
100
|
-
5 => %w[yymdd yymmd ddmyy dmmyy mmdyy mddyy],
|
|
101
|
-
4 => %w[yymd dmyy mdyy]
|
|
102
|
-
}
|
|
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
|
|
103
105
|
|
|
104
106
|
PATTERN_CHAR_TO_SYM = {
|
|
105
107
|
'y' => :year,
|
|
106
108
|
'm' => :month,
|
|
107
109
|
'd' => :day
|
|
108
|
-
}
|
|
110
|
+
}.freeze
|
|
109
111
|
|
|
110
112
|
def date_patterns_for_length(length)
|
|
111
113
|
DATE_PATTERN_FOR_LENGTH[length] || []
|
|
@@ -123,7 +125,7 @@ module Zxcvbn
|
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
def expand_year(year)
|
|
126
|
-
|
|
128
|
+
year
|
|
127
129
|
# Block dates with 2 digit years for now to be compatible with the JS version
|
|
128
130
|
# return year unless year < 100
|
|
129
131
|
# now = Time.now.year
|
|
@@ -20,15 +20,17 @@ module Zxcvbn
|
|
|
20
20
|
(0..password_length).each do |i|
|
|
21
21
|
(i...password_length).each do |j|
|
|
22
22
|
word = lowercased_password[i..j]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
)
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
results
|
data/lib/zxcvbn/matchers/l33t.rb
CHANGED
|
@@ -4,19 +4,19 @@ module Zxcvbn
|
|
|
4
4
|
module Matchers
|
|
5
5
|
class L33t
|
|
6
6
|
L33T_TABLE = {
|
|
7
|
-
'a' => ['4', '@'],
|
|
8
|
-
'b' => ['8'],
|
|
9
|
-
'c' => ['(', '{', '[', '<'],
|
|
10
|
-
'e' => ['3'],
|
|
11
|
-
'g' => ['6', '9'],
|
|
12
|
-
'i' => ['1', '!', '|'],
|
|
13
|
-
'l' => ['1', '|', '7'],
|
|
14
|
-
'o' => ['0'],
|
|
15
|
-
's' => ['$', '5'],
|
|
16
|
-
't' => ['+', '7'],
|
|
17
|
-
'x' => ['%'],
|
|
18
|
-
'z' => ['2']
|
|
19
|
-
}
|
|
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
|
|
20
20
|
|
|
21
21
|
def initialize(dictionary_matchers)
|
|
22
22
|
@dictionary_matchers = dictionary_matchers
|
|
@@ -117,7 +117,7 @@ module Zxcvbn
|
|
|
117
117
|
subs.each do |sub|
|
|
118
118
|
assoc = sub.dup
|
|
119
119
|
|
|
120
|
-
assoc.sort!
|
|
120
|
+
assoc.sort!
|
|
121
121
|
label = assoc.map { |k, v| "#{k},#{v}" }.join('-')
|
|
122
122
|
unless members.include?(label)
|
|
123
123
|
members << label
|
|
@@ -4,19 +4,19 @@ module Zxcvbn
|
|
|
4
4
|
module Matchers
|
|
5
5
|
class L33t
|
|
6
6
|
L33T_TABLE = {
|
|
7
|
-
'a' => ['4', '@'],
|
|
8
|
-
'b' => ['8'],
|
|
9
|
-
'c' => ['(', '{', '[', '<'],
|
|
10
|
-
'e' => ['3'],
|
|
11
|
-
'g' => ['6', '9'],
|
|
12
|
-
'i' => ['1', '!', '|'],
|
|
13
|
-
'l' => ['1', '|', '7'],
|
|
14
|
-
'o' => ['0'],
|
|
15
|
-
's' => ['$', '5'],
|
|
16
|
-
't' => ['+', '7'],
|
|
17
|
-
'x' => ['%'],
|
|
18
|
-
'z' => ['2']
|
|
19
|
-
}
|
|
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
|
|
20
20
|
|
|
21
21
|
def initialize(dictionary_matchers)
|
|
22
22
|
@dictionary_matchers = dictionary_matchers
|
|
@@ -26,17 +26,15 @@ module Zxcvbn
|
|
|
26
26
|
matches = []
|
|
27
27
|
lowercased_password = password.downcase
|
|
28
28
|
combinations_to_try = substitution_combinations(relevant_l33t_substitutions(lowercased_password))
|
|
29
|
-
|
|
30
|
-
combinations_to_try.each do |substitution|
|
|
29
|
+
combinations_to_try.each do |substitutions|
|
|
31
30
|
@dictionary_matchers.each do |matcher|
|
|
32
|
-
subbed_password = substitute(lowercased_password,
|
|
31
|
+
subbed_password = substitute(lowercased_password, substitutions)
|
|
33
32
|
matcher.matches(subbed_password).each do |match|
|
|
34
33
|
token = lowercased_password[match.i..match.j]
|
|
35
34
|
next if token == match.matched_word.downcase
|
|
36
35
|
|
|
37
|
-
# debugger if token == '1'
|
|
38
36
|
match_substitutions = {}
|
|
39
|
-
|
|
37
|
+
substitutions.each do |letter, substitution|
|
|
40
38
|
match_substitutions[substitution] = letter if token.include?(substitution)
|
|
41
39
|
end
|
|
42
40
|
match.l33t = true
|
|
@@ -52,9 +50,9 @@ module Zxcvbn
|
|
|
52
50
|
matches
|
|
53
51
|
end
|
|
54
52
|
|
|
55
|
-
def substitute(password,
|
|
53
|
+
def substitute(password, substitutions)
|
|
56
54
|
subbed_password = password.dup
|
|
57
|
-
|
|
55
|
+
substitutions.each do |letter, substitution|
|
|
58
56
|
subbed_password.gsub!(substitution, letter)
|
|
59
57
|
end
|
|
60
58
|
subbed_password
|
|
@@ -67,9 +65,7 @@ module Zxcvbn
|
|
|
67
65
|
end
|
|
68
66
|
L33T_TABLE.each do |letter, substibutions|
|
|
69
67
|
password.each_char do |password_char|
|
|
70
|
-
if substibutions.include?(password_char)
|
|
71
|
-
subs[letter] << password_char
|
|
72
|
-
end
|
|
68
|
+
subs[letter] << password_char if substibutions.include?(password_char)
|
|
73
69
|
end
|
|
74
70
|
end
|
|
75
71
|
subs
|
|
@@ -97,15 +93,13 @@ module Zxcvbn
|
|
|
97
93
|
end
|
|
98
94
|
|
|
99
95
|
# convert back to simple hash per substitution combination
|
|
100
|
-
|
|
96
|
+
combinations.map do |combination_set|
|
|
101
97
|
hash = {}
|
|
102
98
|
combination_set.each do |combination_hash|
|
|
103
99
|
hash.merge!(combination_hash)
|
|
104
100
|
end
|
|
105
101
|
hash
|
|
106
102
|
end
|
|
107
|
-
|
|
108
|
-
combination_hashes
|
|
109
103
|
end
|
|
110
104
|
|
|
111
105
|
# expand possible combinations if multiple characters can be substituted
|
|
@@ -7,15 +7,15 @@ module Zxcvbn
|
|
|
7
7
|
module RegexHelpers
|
|
8
8
|
def re_match_all(regex, password)
|
|
9
9
|
pos = 0
|
|
10
|
-
while re_match = regex.match(password, pos)
|
|
10
|
+
while (re_match = regex.match(password, pos))
|
|
11
11
|
i, j = re_match.offset(0)
|
|
12
12
|
pos = j
|
|
13
13
|
j -= 1
|
|
14
14
|
|
|
15
15
|
match = Match.new(
|
|
16
|
-
:
|
|
17
|
-
:
|
|
18
|
-
:
|
|
16
|
+
i: i,
|
|
17
|
+
j: j,
|
|
18
|
+
token: password[i..j]
|
|
19
19
|
)
|
|
20
20
|
yield match, re_match
|
|
21
21
|
end
|
|
@@ -11,17 +11,15 @@ module Zxcvbn
|
|
|
11
11
|
while i < password.length
|
|
12
12
|
cur_char = password[i]
|
|
13
13
|
j = i + 1
|
|
14
|
-
while cur_char == password[j]
|
|
15
|
-
j += 1
|
|
16
|
-
end
|
|
14
|
+
j += 1 while cur_char == password[j]
|
|
17
15
|
|
|
18
16
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
19
17
|
result << Match.new(
|
|
20
|
-
:
|
|
21
|
-
:
|
|
22
|
-
:
|
|
23
|
-
:
|
|
24
|
-
:
|
|
18
|
+
pattern: 'repeat',
|
|
19
|
+
i: i,
|
|
20
|
+
j: j - 1,
|
|
21
|
+
token: password[i...j],
|
|
22
|
+
repeated_char: cur_char
|
|
25
23
|
)
|
|
26
24
|
end
|
|
27
25
|
|
|
@@ -9,7 +9,7 @@ module Zxcvbn
|
|
|
9
9
|
'lower' => 'abcdefghijklmnopqrstuvwxyz',
|
|
10
10
|
'upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
11
11
|
'digits' => '01234567890'
|
|
12
|
-
}
|
|
12
|
+
}.freeze
|
|
13
13
|
|
|
14
14
|
def seq_match_length(password, from, direction, seq)
|
|
15
15
|
index_from = seq.index(password[from])
|
|
@@ -27,14 +27,12 @@ module Zxcvbn
|
|
|
27
27
|
SEQUENCES.each do |name, sequence|
|
|
28
28
|
index1 = sequence.index(password[i])
|
|
29
29
|
index2 = sequence.index(password[i + 1])
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
end
|
|
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
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
38
|
|
|
@@ -48,13 +46,13 @@ module Zxcvbn
|
|
|
48
46
|
length = seq_match_length(password, i, seq_direction, seq)
|
|
49
47
|
if length > 2
|
|
50
48
|
result << Match.new(
|
|
51
|
-
:
|
|
52
|
-
:
|
|
53
|
-
:
|
|
54
|
-
:
|
|
55
|
-
:
|
|
56
|
-
:
|
|
57
|
-
:
|
|
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
|
|
58
56
|
)
|
|
59
57
|
end
|
|
60
58
|
i += length - 1
|
|
@@ -36,22 +36,22 @@ module Zxcvbn
|
|
|
36
36
|
cur_char = password[j]
|
|
37
37
|
adjacents.each do |adj|
|
|
38
38
|
cur_direction += 1
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
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
|
|
54
53
|
end
|
|
54
|
+
break
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
# if the current pattern continued, extend j and try to grow again
|
|
@@ -61,13 +61,13 @@ module Zxcvbn
|
|
|
61
61
|
# otherwise push the pattern discovered so far, if any...
|
|
62
62
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
63
63
|
result << Match.new(
|
|
64
|
-
:
|
|
65
|
-
:
|
|
66
|
-
:
|
|
67
|
-
:
|
|
68
|
-
:
|
|
69
|
-
:
|
|
70
|
-
:
|
|
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
|
|
71
71
|
)
|
|
72
72
|
end
|
|
73
73
|
# ...and then start a new search for the rest of the password.
|
data/lib/zxcvbn/matchers/year.rb
CHANGED
data/lib/zxcvbn/math.rb
CHANGED
data/lib/zxcvbn/scorer.rb
CHANGED
|
@@ -24,15 +24,16 @@ module Zxcvbn
|
|
|
24
24
|
backpointers = []
|
|
25
25
|
(0...password.length).each do |k|
|
|
26
26
|
# starting scenario to try and beat: adding a brute-force character to the minimum entropy sequence at k-1.
|
|
27
|
-
previous_k_entropy = k
|
|
27
|
+
previous_k_entropy = k.positive? ? up_to_k[k - 1] : 0
|
|
28
28
|
up_to_k[k] = previous_k_entropy + lg(bruteforce_cardinality)
|
|
29
29
|
backpointers[k] = nil
|
|
30
30
|
matches.select do |match|
|
|
31
31
|
match.j == k
|
|
32
32
|
end.each do |match|
|
|
33
|
-
i
|
|
33
|
+
i = match.i
|
|
34
|
+
j = match.j
|
|
34
35
|
# see if best entropy up to i-1 + entropy of this match is less than the current minimum at j.
|
|
35
|
-
previous_i_entropy = i
|
|
36
|
+
previous_i_entropy = i.positive? ? up_to_k[i - 1] : 0
|
|
36
37
|
candidate_entropy = previous_i_entropy + calc_entropy(match)
|
|
37
38
|
if up_to_k[j] && candidate_entropy < up_to_k[j]
|
|
38
39
|
up_to_k[j] = candidate_entropy
|
|
@@ -58,18 +59,18 @@ module Zxcvbn
|
|
|
58
59
|
score_for(password, match_sequence, up_to_k)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def score_for
|
|
62
|
+
def score_for(password, match_sequence, up_to_k)
|
|
62
63
|
min_entropy = up_to_k[password.length - 1] || 0 # or 0 corner case is for an empty password ''
|
|
63
64
|
crack_time = entropy_to_crack_time(min_entropy)
|
|
64
65
|
|
|
65
66
|
# final result object
|
|
66
67
|
Score.new(
|
|
67
|
-
:
|
|
68
|
-
:
|
|
69
|
-
:
|
|
70
|
-
:
|
|
71
|
-
:
|
|
72
|
-
:
|
|
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)
|
|
73
74
|
)
|
|
74
75
|
end
|
|
75
76
|
|
|
@@ -77,9 +78,7 @@ module Zxcvbn
|
|
|
77
78
|
k = 0
|
|
78
79
|
match_sequence_copy = []
|
|
79
80
|
match_sequence.each do |match|
|
|
80
|
-
if match.i > k
|
|
81
|
-
match_sequence_copy << make_bruteforce_match(password, k, match.i - 1, bruteforce_cardinality)
|
|
82
|
-
end
|
|
81
|
+
match_sequence_copy << make_bruteforce_match(password, k, match.i - 1, bruteforce_cardinality) if match.i > k
|
|
83
82
|
k = match.j + 1
|
|
84
83
|
match_sequence_copy << match
|
|
85
84
|
end
|
|
@@ -94,12 +93,12 @@ module Zxcvbn
|
|
|
94
93
|
# match1.j == match2.i - 1 for every adjacent match1, match2.
|
|
95
94
|
def make_bruteforce_match(password, i, j, bruteforce_cardinality)
|
|
96
95
|
Match.new(
|
|
97
|
-
:
|
|
98
|
-
:
|
|
99
|
-
:
|
|
100
|
-
:
|
|
101
|
-
:
|
|
102
|
-
:
|
|
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
|
|
103
102
|
)
|
|
104
103
|
end
|
|
105
104
|
end
|
data/lib/zxcvbn/tester.rb
CHANGED
data/lib/zxcvbn/version.rb
CHANGED
data/lib/zxcvbn.rb
CHANGED
|
@@ -5,9 +5,9 @@ require 'zxcvbn/version'
|
|
|
5
5
|
require 'zxcvbn/tester'
|
|
6
6
|
|
|
7
7
|
module Zxcvbn
|
|
8
|
-
|
|
8
|
+
module_function
|
|
9
9
|
|
|
10
|
-
DATA_PATH = Pathname(File.expand_path('
|
|
10
|
+
DATA_PATH = Pathname(File.expand_path('../data', __dir__))
|
|
11
11
|
|
|
12
12
|
# Returns a Zxcvbn::Score for the given password
|
|
13
13
|
#
|