zxcvbn-ruby 1.3.0 → 2.0.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 +71 -1
- data/README.md +322 -75
- data/data/frequency_lists/english_wikipedia.txt +30000 -0
- data/data/frequency_lists/female_names.txt +11 -114
- data/data/frequency_lists/male_names.txt +3 -24
- data/data/frequency_lists/passwords.txt +29623 -6764
- data/data/frequency_lists/surnames.txt +28 -30611
- data/data/frequency_lists/{english.txt → us_tv_and_film.txt} +147 -13532
- data/lib/zxcvbn/clock.rb +6 -0
- data/lib/zxcvbn/crack_time.rb +52 -18
- data/lib/zxcvbn/data.rb +61 -21
- data/lib/zxcvbn/dictionary_ranker.rb +10 -0
- data/lib/zxcvbn/feedback.rb +11 -6
- data/lib/zxcvbn/feedback_giver.rb +75 -50
- data/lib/zxcvbn/guesses.rb +208 -0
- data/lib/zxcvbn/match.rb +95 -15
- data/lib/zxcvbn/match_builder.rb +15 -0
- data/lib/zxcvbn/matchers/date.rb +171 -106
- data/lib/zxcvbn/matchers/dictionary.rb +15 -8
- data/lib/zxcvbn/matchers/digits.rb +6 -1
- data/lib/zxcvbn/matchers/l33t.rb +30 -34
- data/lib/zxcvbn/matchers/regex_helpers.rb +14 -6
- data/lib/zxcvbn/matchers/repeat.rb +47 -16
- data/lib/zxcvbn/matchers/sequences.rb +58 -48
- data/lib/zxcvbn/matchers/spatial.rb +22 -6
- data/lib/zxcvbn/matchers/year.rb +6 -1
- data/lib/zxcvbn/math.rb +15 -28
- data/lib/zxcvbn/omnimatch.rb +70 -22
- data/lib/zxcvbn/ruby.rb +3 -0
- data/lib/zxcvbn/score.rb +34 -10
- data/lib/zxcvbn/scorer.rb +142 -75
- data/lib/zxcvbn/tester.rb +58 -23
- data/lib/zxcvbn/tester_builder.rb +83 -0
- data/lib/zxcvbn/trie.rb +21 -0
- data/lib/zxcvbn/version.rb +1 -1
- data/lib/zxcvbn.rb +47 -7
- data/sig/README.md +65 -0
- data/sig/zxcvbn/clock.rbs +5 -0
- data/sig/zxcvbn/crack_time.rbs +11 -0
- data/sig/zxcvbn/data.rbs +40 -0
- data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
- data/sig/zxcvbn/feedback.rbs +10 -0
- data/sig/zxcvbn/feedback_giver.rbs +13 -0
- data/sig/zxcvbn/guesses.rbs +36 -0
- data/sig/zxcvbn/match.rbs +40 -0
- data/sig/zxcvbn/match_builder.rbs +36 -0
- data/sig/zxcvbn/matchers/date.rbs +23 -0
- data/sig/zxcvbn/matchers/dictionary.rbs +21 -0
- data/sig/zxcvbn/matchers/digits.rbs +11 -0
- data/sig/zxcvbn/matchers/l33t.rbs +27 -0
- data/sig/zxcvbn/matchers/regex_helpers.rbs +7 -0
- data/sig/zxcvbn/matchers/repeat.rbs +11 -0
- data/sig/zxcvbn/matchers/sequences.rbs +16 -0
- data/sig/zxcvbn/matchers/spatial.rbs +15 -0
- data/sig/zxcvbn/matchers/year.rbs +11 -0
- data/sig/zxcvbn/math.rbs +9 -0
- data/sig/zxcvbn/omnimatch.rbs +19 -0
- data/sig/zxcvbn/score.rbs +26 -0
- data/sig/zxcvbn/scorer.rbs +19 -0
- data/sig/zxcvbn/tester.rbs +15 -0
- data/sig/zxcvbn/tester_builder.rbs +16 -0
- data/sig/zxcvbn/trie.rbs +17 -0
- data/sig/zxcvbn.rbs +12 -0
- metadata +46 -12
- data/lib/zxcvbn/entropy.rb +0 -158
- data/lib/zxcvbn/matchers/new_l33t.rb +0 -118
- data/lib/zxcvbn/password_strength.rb +0 -27
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'zxcvbn/
|
|
3
|
+
require 'zxcvbn/match_builder'
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
6
|
module Matchers
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
7
|
+
# Matches any sequential segment of the lowercased password that appears in
|
|
8
|
+
# a ranked dictionary.
|
|
9
|
+
# @api private
|
|
10
10
|
class Dictionary
|
|
11
|
+
# @param name [String] dictionary identifier used in match results
|
|
12
|
+
# @param ranked_dictionary [Hash{String => Integer}] lowercased word → rank
|
|
13
|
+
# @param trie [Trie, nil] optional prefix trie for faster lookups
|
|
11
14
|
def initialize(name, ranked_dictionary, trie = nil)
|
|
12
15
|
@name = name
|
|
13
16
|
@ranked_dictionary = ranked_dictionary
|
|
14
17
|
@trie = trie
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
# Returns all dictionary matches found in password.
|
|
21
|
+
#
|
|
22
|
+
# @param password [String]
|
|
23
|
+
# @return [Array<MatchBuilder>] matches with pattern "dictionary"
|
|
17
24
|
def matches(password)
|
|
18
25
|
lowercased_password = password.downcase
|
|
19
26
|
|
|
@@ -56,12 +63,12 @@ module Zxcvbn
|
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def build_match(matched_word, token, start_pos, end_pos, rank)
|
|
59
|
-
|
|
60
|
-
matched_word
|
|
61
|
-
token
|
|
66
|
+
MatchBuilder.new(
|
|
67
|
+
matched_word:,
|
|
68
|
+
token:,
|
|
62
69
|
i: start_pos,
|
|
63
70
|
j: end_pos,
|
|
64
|
-
rank
|
|
71
|
+
rank:,
|
|
65
72
|
pattern: 'dictionary',
|
|
66
73
|
dictionary_name: @name
|
|
67
74
|
)
|
|
@@ -4,11 +4,16 @@ require 'zxcvbn/matchers/regex_helpers'
|
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
6
|
module Matchers
|
|
7
|
+
# Matches runs of 3 or more consecutive digits in the password.
|
|
8
|
+
# @api private
|
|
7
9
|
class Digits
|
|
8
10
|
include RegexHelpers
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
# Matches runs of 3 or more consecutive digits.
|
|
13
|
+
DIGITS_REGEX = /\d{3,}/
|
|
11
14
|
|
|
15
|
+
# @param password [String]
|
|
16
|
+
# @return [Array<MatchBuilder>] matches with pattern "digits"
|
|
12
17
|
def matches(password)
|
|
13
18
|
result = []
|
|
14
19
|
re_match_all(DIGITS_REGEX, password) do |match|
|
data/lib/zxcvbn/matchers/l33t.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'set'
|
|
4
|
-
|
|
5
3
|
module Zxcvbn
|
|
6
4
|
module Matchers
|
|
5
|
+
# Matches dictionary words after substituting common l33t-speak character
|
|
6
|
+
# replacements (e.g. "@" for "a", "3" for "e").
|
|
7
|
+
# @api private
|
|
7
8
|
class L33t
|
|
9
|
+
# Mapping from plain letter to the l33t characters that can represent it.
|
|
8
10
|
L33T_TABLE = {
|
|
9
11
|
'a' => ['4', '@'].freeze,
|
|
10
12
|
'b' => ['8'].freeze,
|
|
@@ -20,10 +22,15 @@ module Zxcvbn
|
|
|
20
22
|
'z' => ['2'].freeze
|
|
21
23
|
}.freeze
|
|
22
24
|
|
|
25
|
+
# @param dictionary_matchers [Array<Dictionary>] matchers to run against substituted passwords
|
|
23
26
|
def initialize(dictionary_matchers)
|
|
24
27
|
@dictionary_matchers = dictionary_matchers
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
# Returns l33t-substituted dictionary matches found in password.
|
|
31
|
+
#
|
|
32
|
+
# @param password [String]
|
|
33
|
+
# @return [Array<MatchBuilder>] matches with pattern "dictionary" and l33t: true
|
|
27
34
|
def matches(password)
|
|
28
35
|
matches = []
|
|
29
36
|
lowercased_password = password.downcase
|
|
@@ -44,14 +51,19 @@ module Zxcvbn
|
|
|
44
51
|
matches
|
|
45
52
|
end
|
|
46
53
|
|
|
54
|
+
# Returns a copy of password with each character replaced according to sub.
|
|
55
|
+
#
|
|
56
|
+
# @param password [String]
|
|
57
|
+
# @param sub [Hash{String => String}] character substitution map
|
|
58
|
+
# @return [String]
|
|
47
59
|
def translate(password, sub)
|
|
48
|
-
|
|
49
|
-
password.each_char do |chr|
|
|
50
|
-
result << (sub[chr] || chr)
|
|
51
|
-
end
|
|
52
|
-
result
|
|
60
|
+
password.gsub(Regexp.union(sub.keys), sub)
|
|
53
61
|
end
|
|
54
62
|
|
|
63
|
+
# Returns the subset of {L33T_TABLE} whose l33t characters appear in password.
|
|
64
|
+
#
|
|
65
|
+
# @param password [String] lowercased password
|
|
66
|
+
# @return [Hash{String => Array<String>}]
|
|
55
67
|
def relevent_l33t_subtable(password)
|
|
56
68
|
filtered = {}
|
|
57
69
|
L33T_TABLE.each do |letter, subs|
|
|
@@ -61,19 +73,15 @@ module Zxcvbn
|
|
|
61
73
|
filtered
|
|
62
74
|
end
|
|
63
75
|
|
|
76
|
+
# Enumerates all possible substitution combinations for the given l33t subtable.
|
|
77
|
+
#
|
|
78
|
+
# @param table [Hash{String => Array<String>}] relevant l33t subtable
|
|
79
|
+
# @return [Array<Hash{String => String}>] list of substitution maps to try
|
|
64
80
|
def l33t_subs(table)
|
|
65
81
|
keys = table.keys
|
|
66
82
|
subs = [[]]
|
|
67
83
|
subs = find_substitutions(subs, table, keys)
|
|
68
|
-
|
|
69
|
-
subs.each do |sub|
|
|
70
|
-
hash = {}
|
|
71
|
-
sub.each do |l33t_char, chr|
|
|
72
|
-
hash[l33t_char] = chr
|
|
73
|
-
end
|
|
74
|
-
new_subs << hash
|
|
75
|
-
end
|
|
76
|
-
new_subs
|
|
84
|
+
subs.map(&:to_h)
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
private
|
|
@@ -83,16 +91,11 @@ module Zxcvbn
|
|
|
83
91
|
token = password.slice(match.i, length)
|
|
84
92
|
return if token.downcase == match.matched_word.downcase
|
|
85
93
|
|
|
86
|
-
match_substitutions = {}
|
|
87
|
-
substitution.each do |s, letter|
|
|
88
|
-
match_substitutions[s] = letter if token.include?(s)
|
|
89
|
-
end
|
|
94
|
+
match_substitutions = substitution.select { |s, _| token.include?(s) }
|
|
90
95
|
match.l33t = true
|
|
91
96
|
match.token = token
|
|
92
97
|
match.sub = match_substitutions
|
|
93
|
-
match.sub_display = match_substitutions.map
|
|
94
|
-
"#{k} -> #{v}"
|
|
95
|
-
end.join(', ')
|
|
98
|
+
match.sub_display = match_substitutions.map { |k, v| "#{k} -> #{v}" }.join(', ')
|
|
96
99
|
matches << match
|
|
97
100
|
end
|
|
98
101
|
|
|
@@ -100,21 +103,14 @@ module Zxcvbn
|
|
|
100
103
|
return subs if keys.empty?
|
|
101
104
|
|
|
102
105
|
first_key = keys[0]
|
|
103
|
-
rest_keys = keys[1
|
|
106
|
+
rest_keys = keys[1..]
|
|
104
107
|
next_subs = []
|
|
105
108
|
table[first_key].each do |l33t_char|
|
|
106
109
|
subs.each do |sub|
|
|
107
|
-
dup_l33t_index =
|
|
108
|
-
(0...sub.length).each do |i|
|
|
109
|
-
if sub[i][0] == l33t_char
|
|
110
|
-
dup_l33t_index = i
|
|
111
|
-
break
|
|
112
|
-
end
|
|
113
|
-
end
|
|
110
|
+
dup_l33t_index = sub.find_index { |pair| pair[0] == l33t_char }
|
|
114
111
|
|
|
115
|
-
if dup_l33t_index
|
|
116
|
-
|
|
117
|
-
next_subs << sub_extension
|
|
112
|
+
if dup_l33t_index.nil?
|
|
113
|
+
next_subs << (sub + [[l33t_char, first_key]])
|
|
118
114
|
else
|
|
119
115
|
sub_alternative = sub.dup
|
|
120
116
|
sub_alternative[dup_l33t_index, 1] = [[l33t_char, first_key]]
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'zxcvbn/
|
|
3
|
+
require 'zxcvbn/match_builder'
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
|
+
# Namespace for all password pattern matchers.
|
|
7
|
+
# @api private
|
|
6
8
|
module Matchers
|
|
9
|
+
# Shared helper for iterating non-overlapping regex matches over a password.
|
|
10
|
+
# @api private
|
|
7
11
|
module RegexHelpers
|
|
12
|
+
# Yields a {Match} and the underlying MatchData for every non-overlapping
|
|
13
|
+
# occurrence of regex in password.
|
|
14
|
+
#
|
|
15
|
+
# @param regex [Regexp] pattern to search for
|
|
16
|
+
# @param password [String] the password to search
|
|
17
|
+
# @yieldparam match [MatchBuilder] match with i, j, and token set
|
|
18
|
+
# @yieldparam re_match [MatchData] the underlying MatchData object
|
|
19
|
+
# @return [void]
|
|
8
20
|
def re_match_all(regex, password)
|
|
9
21
|
pos = 0
|
|
10
22
|
while (re_match = regex.match(password, pos))
|
|
@@ -12,11 +24,7 @@ module Zxcvbn
|
|
|
12
24
|
pos = j
|
|
13
25
|
j -= 1
|
|
14
26
|
|
|
15
|
-
match =
|
|
16
|
-
i: i,
|
|
17
|
-
j: j,
|
|
18
|
-
token: password.slice(i, j - i + 1)
|
|
19
|
-
)
|
|
27
|
+
match = MatchBuilder.new(i:, j:, token: password.slice(i, j - i + 1))
|
|
20
28
|
yield match, re_match
|
|
21
29
|
end
|
|
22
30
|
end
|
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'zxcvbn/
|
|
3
|
+
require 'zxcvbn/match_builder'
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
6
|
module Matchers
|
|
7
|
+
# Finds repeated substrings in a password (e.g. "abcabc", "aaaa").
|
|
8
|
+
#
|
|
9
|
+
# Uses a greedy/lazy regex disambiguation strategy from zxcvbn.js v4:
|
|
10
|
+
# prefer the greedier match unless the lazy match is longer, then use
|
|
11
|
+
# LAZY_ANCHORED to extract the minimal repeating unit (base_token).
|
|
12
|
+
# @api private
|
|
7
13
|
class Repeat
|
|
14
|
+
# Greedily matches the longest repeated substring.
|
|
15
|
+
GREEDY = /(.+)\1+/
|
|
16
|
+
# Lazily matches the shortest repeated substring.
|
|
17
|
+
LAZY = /(.+?)\1+/
|
|
18
|
+
# Anchored lazy pattern used to extract the minimal base token.
|
|
19
|
+
LAZY_ANCHORED = /^(.+?)\1+$/
|
|
20
|
+
|
|
21
|
+
# Find all repeated-substring matches in the password.
|
|
22
|
+
#
|
|
23
|
+
# @param password [String] the password to search
|
|
24
|
+
# @return [Array<MatchBuilder>] matches with pattern 'repeat', each containing
|
|
25
|
+
# base_token (the repeated unit) and repeat_count
|
|
8
26
|
def matches(password)
|
|
9
27
|
result = []
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
28
|
+
last_index = 0
|
|
29
|
+
|
|
30
|
+
while last_index < password.length
|
|
31
|
+
greedy_match = GREEDY.match(password, last_index)
|
|
32
|
+
lazy_match = LAZY.match(password, last_index)
|
|
33
|
+
break unless greedy_match
|
|
34
|
+
|
|
35
|
+
if greedy_match[0].length > lazy_match[0].length
|
|
36
|
+
rx_match = greedy_match
|
|
37
|
+
base_token = LAZY_ANCHORED.match(rx_match[0])[1]
|
|
38
|
+
else
|
|
39
|
+
rx_match = lazy_match
|
|
40
|
+
base_token = rx_match[1]
|
|
24
41
|
end
|
|
25
42
|
|
|
26
|
-
i
|
|
43
|
+
i = rx_match.begin(0)
|
|
44
|
+
j = rx_match.end(0) - 1
|
|
45
|
+
token = rx_match[0]
|
|
46
|
+
|
|
47
|
+
result << MatchBuilder.new(
|
|
48
|
+
pattern: 'repeat',
|
|
49
|
+
i:,
|
|
50
|
+
j:,
|
|
51
|
+
token:,
|
|
52
|
+
base_token:,
|
|
53
|
+
repeat_count: token.length / base_token.length
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
last_index = j + 1
|
|
27
57
|
end
|
|
58
|
+
|
|
28
59
|
result
|
|
29
60
|
end
|
|
30
61
|
end
|
|
@@ -1,67 +1,77 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'zxcvbn/
|
|
3
|
+
require 'zxcvbn/match_builder'
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
6
|
module Matchers
|
|
7
|
+
# Matches monotonically incrementing or decrementing character sequences,
|
|
8
|
+
# such as "abcde", "54321", or "ZYXW".
|
|
9
|
+
# @api private
|
|
7
10
|
class Sequences
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
# Maximum absolute step between adjacent characters for a valid sequence.
|
|
12
|
+
MAX_DELTA = 5
|
|
13
|
+
# Matches tokens that are all lowercase letters.
|
|
14
|
+
ALL_LOWER = /^[a-z]+$/
|
|
15
|
+
# Matches tokens that are all uppercase letters.
|
|
16
|
+
ALL_UPPER = /^[A-Z]+$/
|
|
17
|
+
# Matches tokens that are all digits.
|
|
18
|
+
ALL_DIGITS = /^\d+$/
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
password[from + j] == seq[index_from + direction * j]
|
|
19
|
-
j += 1
|
|
20
|
-
end
|
|
21
|
-
j
|
|
22
|
-
end
|
|
20
|
+
# @param password [String]
|
|
21
|
+
# @return [Array<MatchBuilder>] matches with pattern "sequence"
|
|
22
|
+
def matches(password)
|
|
23
|
+
return [] if password.length < 2
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
SEQUENCES.each do |name, sequence|
|
|
28
|
-
index1 = sequence.index(password[i])
|
|
29
|
-
index2 = sequence.index(password[i + 1])
|
|
30
|
-
next unless index1 && index2
|
|
25
|
+
result = []
|
|
26
|
+
start = 0
|
|
27
|
+
last_delta = nil
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
return
|
|
29
|
+
emit = lambda do |seq_end, delta|
|
|
30
|
+
return if delta.nil?
|
|
34
31
|
|
|
35
|
-
|
|
32
|
+
abs_delta = delta.abs
|
|
33
|
+
return unless abs_delta.positive? && abs_delta <= MAX_DELTA
|
|
34
|
+
|
|
35
|
+
len = seq_end - start + 1
|
|
36
|
+
return unless len > 2 || abs_delta == 1
|
|
37
|
+
|
|
38
|
+
token = password[start, len]
|
|
39
|
+
seq_name, seq_space = classify(token)
|
|
40
|
+
result << MatchBuilder.new(
|
|
41
|
+
pattern: 'sequence',
|
|
42
|
+
i: start,
|
|
43
|
+
j: seq_end,
|
|
44
|
+
token:,
|
|
45
|
+
sequence_name: seq_name,
|
|
46
|
+
sequence_space: seq_space,
|
|
47
|
+
ascending: delta.positive?
|
|
48
|
+
)
|
|
36
49
|
end
|
|
37
|
-
end
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
seq_name, seq, seq_direction = applicable_sequence(password, i)
|
|
51
|
+
(1...password.length).each do |i|
|
|
52
|
+
delta = password[i].ord - password[i - 1].ord
|
|
53
|
+
last_delta = delta if last_delta.nil?
|
|
54
|
+
next if delta == last_delta
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
result << Match.new(
|
|
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
|
-
)
|
|
57
|
-
end
|
|
58
|
-
i += length - 1
|
|
59
|
-
else
|
|
60
|
-
i += 1
|
|
61
|
-
end
|
|
56
|
+
emit.call(i - 1, last_delta)
|
|
57
|
+
start = i - 1
|
|
58
|
+
last_delta = delta
|
|
62
59
|
end
|
|
60
|
+
emit.call(password.length - 1, last_delta)
|
|
61
|
+
|
|
63
62
|
result
|
|
64
63
|
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def classify(token)
|
|
68
|
+
case token
|
|
69
|
+
when ALL_LOWER then ['lower', 26]
|
|
70
|
+
when ALL_UPPER then ['upper', 26]
|
|
71
|
+
when ALL_DIGITS then ['digits', 10]
|
|
72
|
+
else ['unicode', 26]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
65
75
|
end
|
|
66
76
|
end
|
|
67
77
|
end
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'zxcvbn/
|
|
3
|
+
require 'zxcvbn/match_builder'
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
6
|
module Matchers
|
|
7
|
+
# Matches keyboard spatial patterns (e.g. "qwerty", "asdf") across all
|
|
8
|
+
# configured adjacency graphs.
|
|
9
|
+
# @api private
|
|
7
10
|
class Spatial
|
|
11
|
+
# Matches characters that require the Shift key on a standard keyboard.
|
|
12
|
+
SHIFTED_RX = /[~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?]/
|
|
13
|
+
|
|
14
|
+
# @param graphs [Hash{String => Hash}] adjacency graph data keyed by graph name
|
|
8
15
|
def initialize(graphs)
|
|
9
16
|
@graphs = graphs
|
|
10
17
|
end
|
|
11
18
|
|
|
19
|
+
# @param password [String]
|
|
20
|
+
# @return [Array<MatchBuilder>] matches with pattern "spatial" across all graphs
|
|
12
21
|
def matches(password)
|
|
13
22
|
results = []
|
|
14
23
|
@graphs.each do |graph_name, graph|
|
|
@@ -17,14 +26,21 @@ module Zxcvbn
|
|
|
17
26
|
results
|
|
18
27
|
end
|
|
19
28
|
|
|
29
|
+
# Returns spatial matches found in password using a single adjacency graph.
|
|
30
|
+
#
|
|
31
|
+
# @param graph [Hash] adjacency map for each key character
|
|
32
|
+
# @param graph_name [String] name of the graph (e.g. "qwerty")
|
|
33
|
+
# @param password [String]
|
|
34
|
+
# @return [Array<MatchBuilder>] matches with pattern "spatial"
|
|
20
35
|
def matches_for_graph(graph, graph_name, password)
|
|
21
36
|
result = []
|
|
37
|
+
keyboard_graph = %w[qwerty dvorak].include?(graph_name)
|
|
22
38
|
i = 0
|
|
23
39
|
while i < password.length - 1
|
|
24
40
|
j = i + 1
|
|
25
41
|
last_direction = nil
|
|
26
42
|
turns = 0
|
|
27
|
-
shifted_count = 0
|
|
43
|
+
shifted_count = keyboard_graph && SHIFTED_RX.match?(password[i]) ? 1 : 0
|
|
28
44
|
loop do
|
|
29
45
|
prev_char = password[j - 1]
|
|
30
46
|
found = false
|
|
@@ -60,14 +76,14 @@ module Zxcvbn
|
|
|
60
76
|
else
|
|
61
77
|
# otherwise push the pattern discovered so far, if any...
|
|
62
78
|
if j - i > 2 # don't consider length 1 or 2 chains.
|
|
63
|
-
result <<
|
|
79
|
+
result << MatchBuilder.new(
|
|
64
80
|
pattern: 'spatial',
|
|
65
|
-
i
|
|
81
|
+
i:,
|
|
66
82
|
j: j - 1,
|
|
67
83
|
token: password.slice(i, j - i),
|
|
68
84
|
graph: graph_name,
|
|
69
|
-
turns
|
|
70
|
-
shifted_count:
|
|
85
|
+
turns:,
|
|
86
|
+
shifted_count:
|
|
71
87
|
)
|
|
72
88
|
end
|
|
73
89
|
# ...and then start a new search for the rest of the password.
|
data/lib/zxcvbn/matchers/year.rb
CHANGED
|
@@ -4,11 +4,16 @@ require 'zxcvbn/matchers/regex_helpers'
|
|
|
4
4
|
|
|
5
5
|
module Zxcvbn
|
|
6
6
|
module Matchers
|
|
7
|
+
# Matches 4-digit year substrings (1900–2019) in the password.
|
|
8
|
+
# @api private
|
|
7
9
|
class Year
|
|
8
10
|
include RegexHelpers
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
# Matches years from 1900 to 2019, matching zxcvbn.js v4.
|
|
13
|
+
YEAR_REGEX = /19\d\d|200\d|201\d/
|
|
11
14
|
|
|
15
|
+
# @param password [String]
|
|
16
|
+
# @return [Array<MatchBuilder>] matches with pattern "year"
|
|
12
17
|
def matches(password)
|
|
13
18
|
result = []
|
|
14
19
|
re_match_all(YEAR_REGEX, password) do |match|
|
data/lib/zxcvbn/math.rb
CHANGED
|
@@ -1,35 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zxcvbn
|
|
4
|
+
# Mathematical utilities used by the guess estimation logic.
|
|
5
|
+
# @api private
|
|
4
6
|
module Math
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
when (48..57)
|
|
11
|
-
is_type_of['digits'] = true
|
|
12
|
-
when (65..90)
|
|
13
|
-
is_type_of['upper'] = true
|
|
14
|
-
when (97..122)
|
|
15
|
-
is_type_of['lower'] = true
|
|
16
|
-
else
|
|
17
|
-
is_type_of['symbols'] = true
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
cardinality = 0
|
|
22
|
-
cardinality += 10 if is_type_of['digits']
|
|
23
|
-
cardinality += 26 if is_type_of['upper']
|
|
24
|
-
cardinality += 26 if is_type_of['lower']
|
|
25
|
-
cardinality += 33 if is_type_of['symbols']
|
|
26
|
-
cardinality
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def lg(n)
|
|
30
|
-
::Math.log(n, 2)
|
|
31
|
-
end
|
|
32
|
-
|
|
7
|
+
# Computes the binomial coefficient C(n, k) ("n choose k").
|
|
8
|
+
#
|
|
9
|
+
# @param n [Integer]
|
|
10
|
+
# @param k [Integer]
|
|
11
|
+
# @return [Integer]
|
|
33
12
|
def nCk(n, k)
|
|
34
13
|
return 0 if k > n
|
|
35
14
|
return 1 if k.zero?
|
|
@@ -47,10 +26,18 @@ module Zxcvbn
|
|
|
47
26
|
r
|
|
48
27
|
end
|
|
49
28
|
|
|
29
|
+
# Returns the precomputed average key-adjacency degree for a keyboard graph.
|
|
30
|
+
#
|
|
31
|
+
# @param graph_name [String] e.g. "qwerty" or "keypad"
|
|
32
|
+
# @return [Float]
|
|
50
33
|
def average_degree_for_graph(graph_name)
|
|
51
34
|
data.graph_stats[graph_name][:average_degree]
|
|
52
35
|
end
|
|
53
36
|
|
|
37
|
+
# Returns the number of starting positions (keys) in a keyboard graph.
|
|
38
|
+
#
|
|
39
|
+
# @param graph_name [String] e.g. "qwerty" or "keypad"
|
|
40
|
+
# @return [Integer]
|
|
54
41
|
def starting_positions_for_graph(graph_name)
|
|
55
42
|
data.graph_stats[graph_name][:starting_positions]
|
|
56
43
|
end
|