zxcvbn-ruby 1.2.4 → 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 +33 -1
- data/lib/zxcvbn/data.rb +33 -2
- data/lib/zxcvbn/dictionary_ranker.rb +3 -4
- data/lib/zxcvbn/match.rb +17 -5
- data/lib/zxcvbn/matchers/dictionary.rb +43 -12
- data/lib/zxcvbn/matchers/l33t.rb +39 -25
- data/lib/zxcvbn/matchers/new_l33t.rb +3 -2
- data/lib/zxcvbn/matchers/regex_helpers.rb +1 -1
- data/lib/zxcvbn/matchers/repeat.rb +1 -1
- data/lib/zxcvbn/matchers/spatial.rb +1 -1
- data/lib/zxcvbn/math.rb +6 -5
- data/lib/zxcvbn/omnimatch.rb +2 -1
- data/lib/zxcvbn/scorer.rb +1 -1
- data/lib/zxcvbn/trie.rb +44 -0
- data/lib/zxcvbn/version.rb +1 -1
- 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 +23 -5
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,39 @@ 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
|
|
10
42
|
|
|
11
43
|
## [1.2.4] - 2025-12-07
|
|
12
44
|
|
data/lib/zxcvbn/data.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'zxcvbn/dictionary_ranker'
|
|
5
|
+
require 'zxcvbn/trie'
|
|
5
6
|
|
|
6
7
|
module Zxcvbn
|
|
7
8
|
class Data
|
|
@@ -14,12 +15,16 @@ module Zxcvbn
|
|
|
14
15
|
'surnames' => read_word_list('surnames.txt')
|
|
15
16
|
)
|
|
16
17
|
@adjacency_graphs = JSON.parse(DATA_PATH.join('adjacency_graphs.json').read)
|
|
18
|
+
@dictionary_tries = build_tries
|
|
19
|
+
@graph_stats = compute_graph_stats
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
attr_reader :ranked_dictionaries, :adjacency_graphs
|
|
22
|
+
attr_reader :ranked_dictionaries, :adjacency_graphs, :dictionary_tries, :graph_stats
|
|
20
23
|
|
|
21
24
|
def add_word_list(name, list)
|
|
22
|
-
|
|
25
|
+
ranked_dict = DictionaryRanker.rank_dictionary(list)
|
|
26
|
+
@ranked_dictionaries[name] = ranked_dict
|
|
27
|
+
@dictionary_tries[name] = build_trie(ranked_dict)
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
private
|
|
@@ -27,5 +32,31 @@ module Zxcvbn
|
|
|
27
32
|
def read_word_list(file)
|
|
28
33
|
DATA_PATH.join('frequency_lists', file).read.split
|
|
29
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
|
|
60
|
+
end
|
|
30
61
|
end
|
|
31
62
|
end
|
|
@@ -9,10 +9,9 @@ module Zxcvbn
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def self.rank_dictionary(words)
|
|
12
|
-
words
|
|
13
|
-
|
|
14
|
-
dictionary[word.downcase] = i + 1
|
|
15
|
-
end
|
|
12
|
+
words
|
|
13
|
+
.each_with_index
|
|
14
|
+
.with_object({}) { |(word, i), dictionary| dictionary[word.downcase] = i + 1 }
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
end
|
data/lib/zxcvbn/match.rb
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'ostruct'
|
|
4
|
-
|
|
5
3
|
module Zxcvbn
|
|
6
|
-
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
|
+
|
|
7
18
|
def to_hash
|
|
8
|
-
|
|
9
|
-
|
|
19
|
+
instance_variables.sort.each_with_object({}) do |var, hash|
|
|
20
|
+
key = var.to_s.delete_prefix('@')
|
|
21
|
+
hash[key] = instance_variable_get(var)
|
|
10
22
|
end
|
|
11
23
|
end
|
|
12
24
|
end
|
|
@@ -8,33 +8,64 @@ module Zxcvbn
|
|
|
8
8
|
# the lowercased password in the dictionary
|
|
9
9
|
|
|
10
10
|
class Dictionary
|
|
11
|
-
def initialize(name, ranked_dictionary)
|
|
11
|
+
def initialize(name, ranked_dictionary, trie = nil)
|
|
12
12
|
@name = name
|
|
13
13
|
@ranked_dictionary = ranked_dictionary
|
|
14
|
+
@trie = trie
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def matches(password)
|
|
18
|
+
lowercased_password = password.downcase
|
|
19
|
+
|
|
20
|
+
if @trie
|
|
21
|
+
trie_matches(password, lowercased_password)
|
|
22
|
+
else
|
|
23
|
+
hash_matches(password, lowercased_password)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def trie_matches(password, lowercased_password)
|
|
30
|
+
results = []
|
|
31
|
+
|
|
32
|
+
(0...password.length).each do |i|
|
|
33
|
+
@trie.search_prefixes(lowercased_password, i).each do |word, rank, start, ending|
|
|
34
|
+
results << build_match(word, password.slice(start, ending - start + 1), start, ending, rank)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
results
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hash_matches(password, lowercased_password)
|
|
17
42
|
results = []
|
|
18
43
|
password_length = password.length
|
|
19
|
-
|
|
44
|
+
|
|
20
45
|
(0..password_length).each do |i|
|
|
21
46
|
(i...password_length).each do |j|
|
|
22
|
-
|
|
47
|
+
length = j - i + 1
|
|
48
|
+
word = lowercased_password.slice(i, length)
|
|
23
49
|
next unless @ranked_dictionary.key?(word)
|
|
24
50
|
|
|
25
|
-
results <<
|
|
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
|
-
)
|
|
51
|
+
results << build_match(word, password.slice(i, length), i, j, @ranked_dictionary[word])
|
|
34
52
|
end
|
|
35
53
|
end
|
|
54
|
+
|
|
36
55
|
results
|
|
37
56
|
end
|
|
57
|
+
|
|
58
|
+
def build_match(matched_word, token, start_pos, end_pos, rank)
|
|
59
|
+
Match.new(
|
|
60
|
+
matched_word: matched_word,
|
|
61
|
+
token: token,
|
|
62
|
+
i: start_pos,
|
|
63
|
+
j: end_pos,
|
|
64
|
+
rank: rank,
|
|
65
|
+
pattern: 'dictionary',
|
|
66
|
+
dictionary_name: @name
|
|
67
|
+
)
|
|
68
|
+
end
|
|
38
69
|
end
|
|
39
70
|
end
|
|
40
71
|
end
|
data/lib/zxcvbn/matchers/l33t.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module Zxcvbn
|
|
4
6
|
module Matchers
|
|
5
7
|
class L33t
|
|
@@ -25,25 +27,17 @@ module Zxcvbn
|
|
|
25
27
|
def matches(password)
|
|
26
28
|
matches = []
|
|
27
29
|
lowercased_password = password.downcase
|
|
28
|
-
|
|
30
|
+
relevent_subtable = relevent_l33t_subtable(lowercased_password)
|
|
31
|
+
|
|
32
|
+
# Early bailout: if no l33t characters present, return empty matches
|
|
33
|
+
return matches if relevent_subtable.empty?
|
|
34
|
+
|
|
35
|
+
combinations_to_try = l33t_subs(relevent_subtable)
|
|
29
36
|
combinations_to_try.each do |substitution|
|
|
30
37
|
@dictionary_matchers.each do |matcher|
|
|
31
38
|
subbed_password = translate(lowercased_password, substitution)
|
|
32
39
|
matcher.matches(subbed_password).each do |match|
|
|
33
|
-
|
|
34
|
-
next if token.downcase == match.matched_word.downcase
|
|
35
|
-
|
|
36
|
-
match_substitutions = {}
|
|
37
|
-
substitution.each do |s, letter|
|
|
38
|
-
match_substitutions[s] = letter if token.include?(s)
|
|
39
|
-
end
|
|
40
|
-
match.l33t = true
|
|
41
|
-
match.token = password[match.i..match.j]
|
|
42
|
-
match.sub = match_substitutions
|
|
43
|
-
match.sub_display = match_substitutions.map do |k, v|
|
|
44
|
-
"#{k} -> #{v}"
|
|
45
|
-
end.join(', ')
|
|
46
|
-
matches << match
|
|
40
|
+
process_match(match, password, substitution, matches)
|
|
47
41
|
end
|
|
48
42
|
end
|
|
49
43
|
end
|
|
@@ -51,9 +45,11 @@ module Zxcvbn
|
|
|
51
45
|
end
|
|
52
46
|
|
|
53
47
|
def translate(password, sub)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
result = String.new
|
|
49
|
+
password.each_char do |chr|
|
|
50
|
+
result << (sub[chr] || chr)
|
|
51
|
+
end
|
|
52
|
+
result
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
def relevent_l33t_subtable(password)
|
|
@@ -80,6 +76,26 @@ module Zxcvbn
|
|
|
80
76
|
new_subs
|
|
81
77
|
end
|
|
82
78
|
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def process_match(match, password, substitution, matches)
|
|
82
|
+
length = match.j - match.i + 1
|
|
83
|
+
token = password.slice(match.i, length)
|
|
84
|
+
return if token.downcase == match.matched_word.downcase
|
|
85
|
+
|
|
86
|
+
match_substitutions = {}
|
|
87
|
+
substitution.each do |s, letter|
|
|
88
|
+
match_substitutions[s] = letter if token.include?(s)
|
|
89
|
+
end
|
|
90
|
+
match.l33t = true
|
|
91
|
+
match.token = token
|
|
92
|
+
match.sub = match_substitutions
|
|
93
|
+
match.sub_display = match_substitutions.map do |k, v|
|
|
94
|
+
"#{k} -> #{v}"
|
|
95
|
+
end.join(', ')
|
|
96
|
+
matches << match
|
|
97
|
+
end
|
|
98
|
+
|
|
83
99
|
def find_substitutions(subs, table, keys)
|
|
84
100
|
return subs if keys.empty?
|
|
85
101
|
|
|
@@ -113,14 +129,12 @@ module Zxcvbn
|
|
|
113
129
|
|
|
114
130
|
def dedup(subs)
|
|
115
131
|
deduped = []
|
|
116
|
-
|
|
132
|
+
seen = Set.new
|
|
117
133
|
subs.each do |sub|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
unless members.include?(label)
|
|
123
|
-
members << label
|
|
134
|
+
# Sort and convert to hash for consistent comparison
|
|
135
|
+
sorted_sub = sub.sort.to_h
|
|
136
|
+
unless seen.include?(sorted_sub)
|
|
137
|
+
seen.add(sorted_sub)
|
|
124
138
|
deduped << sub
|
|
125
139
|
end
|
|
126
140
|
end
|
|
@@ -30,7 +30,8 @@ module Zxcvbn
|
|
|
30
30
|
@dictionary_matchers.each do |matcher|
|
|
31
31
|
subbed_password = substitute(lowercased_password, substitutions)
|
|
32
32
|
matcher.matches(subbed_password).each do |match|
|
|
33
|
-
|
|
33
|
+
length = match.j - match.i + 1
|
|
34
|
+
token = lowercased_password.slice(match.i, length)
|
|
34
35
|
next if token == match.matched_word.downcase
|
|
35
36
|
|
|
36
37
|
match_substitutions = {}
|
|
@@ -38,7 +39,7 @@ module Zxcvbn
|
|
|
38
39
|
match_substitutions[substitution] = letter if token.include?(substitution)
|
|
39
40
|
end
|
|
40
41
|
match.l33t = true
|
|
41
|
-
match.token = password
|
|
42
|
+
match.token = password.slice(match.i, length)
|
|
42
43
|
match.sub = match_substitutions
|
|
43
44
|
match.sub_display = match_substitutions.map do |k, v|
|
|
44
45
|
"#{k} -> #{v}"
|
data/lib/zxcvbn/math.rb
CHANGED
|
@@ -34,6 +34,10 @@ module Zxcvbn
|
|
|
34
34
|
return 0 if k > n
|
|
35
35
|
return 1 if k.zero?
|
|
36
36
|
|
|
37
|
+
# Use symmetry property: C(n,k) = C(n, n-k)
|
|
38
|
+
# Choose smaller k to minimize iterations
|
|
39
|
+
k = n - k if k > n - k
|
|
40
|
+
|
|
37
41
|
r = 1
|
|
38
42
|
(1..k).each do |d|
|
|
39
43
|
r *= n
|
|
@@ -44,14 +48,11 @@ module Zxcvbn
|
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
def average_degree_for_graph(graph_name)
|
|
47
|
-
|
|
48
|
-
degrees = graph.map { |_, neighbors| neighbors.compact.size }
|
|
49
|
-
sum = degrees.inject(0, :+)
|
|
50
|
-
sum.to_f / graph.size
|
|
51
|
+
data.graph_stats[graph_name][:average_degree]
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def starting_positions_for_graph(graph_name)
|
|
54
|
-
data.
|
|
55
|
+
data.graph_stats[graph_name][:starting_positions]
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
end
|
data/lib/zxcvbn/omnimatch.rb
CHANGED
|
@@ -38,7 +38,8 @@ module Zxcvbn
|
|
|
38
38
|
def build_matchers
|
|
39
39
|
matchers = []
|
|
40
40
|
dictionary_matchers = @data.ranked_dictionaries.map do |name, dictionary|
|
|
41
|
-
|
|
41
|
+
trie = @data.dictionary_tries[name]
|
|
42
|
+
Matchers::Dictionary.new(name, dictionary, trie)
|
|
42
43
|
end
|
|
43
44
|
l33t_matcher = Matchers::L33t.new(dictionary_matchers)
|
|
44
45
|
matchers += dictionary_matchers
|
data/lib/zxcvbn/scorer.rb
CHANGED
data/lib/zxcvbn/trie.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zxcvbn
|
|
4
|
+
# A trie (prefix tree) data structure for efficient dictionary matching.
|
|
5
|
+
# Provides fast prefix-based lookups to eliminate unnecessary substring checks.
|
|
6
|
+
#
|
|
7
|
+
# @see https://en.wikipedia.org/wiki/Trie
|
|
8
|
+
class Trie
|
|
9
|
+
def initialize
|
|
10
|
+
@root = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Insert a word and its rank into the trie
|
|
14
|
+
# @param word [String] the word to insert
|
|
15
|
+
# @param rank [Integer] the rank/frequency of the word
|
|
16
|
+
def insert(word, rank)
|
|
17
|
+
node = @root
|
|
18
|
+
word.each_char do |char|
|
|
19
|
+
node[char] ||= {}
|
|
20
|
+
node = node[char]
|
|
21
|
+
end
|
|
22
|
+
node[:rank] = rank
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Search for all words in the text starting from a given position
|
|
26
|
+
# @param text [String] the text to search in
|
|
27
|
+
# @param start_pos [Integer] the starting position
|
|
28
|
+
# @return [Array<Array>] array of [word, rank, start, end] tuples
|
|
29
|
+
def search_prefixes(text, start_pos)
|
|
30
|
+
results = []
|
|
31
|
+
node = @root
|
|
32
|
+
|
|
33
|
+
(start_pos...text.length).each do |i|
|
|
34
|
+
char = text[i]
|
|
35
|
+
break unless node[char]
|
|
36
|
+
|
|
37
|
+
node = node[char]
|
|
38
|
+
results << [text[start_pos..i], node[:rank], start_pos, i] if node[:rank]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
results
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/zxcvbn/version.rb
CHANGED
data/sig/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# RBS Type Signatures
|
|
2
|
+
|
|
3
|
+
This directory contains [RBS](https://github.com/ruby/rbs) type signatures for the zxcvbn-ruby gem.
|
|
4
|
+
|
|
5
|
+
## What is RBS?
|
|
6
|
+
|
|
7
|
+
RBS is Ruby's type signature language. It provides a way to describe the structure of Ruby programs with:
|
|
8
|
+
- Class and module definitions
|
|
9
|
+
- Method signatures with parameter and return types
|
|
10
|
+
- Instance variables and constants
|
|
11
|
+
- Duck typing and union types
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Validating Type Signatures
|
|
16
|
+
|
|
17
|
+
To validate that the RBS files are syntactically correct:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle exec rake rbs:validate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Runtime Type Checking
|
|
24
|
+
|
|
25
|
+
To run runtime type checking against the actual Ruby code during tests:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle exec rake rbs:test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This runs the RSpec test suite with RBS type checking enabled, verifying that method calls match their type signatures at runtime. Note: This takes about 2 minutes to run.
|
|
32
|
+
|
|
33
|
+
### Other Useful Commands
|
|
34
|
+
|
|
35
|
+
List all Zxcvbn types:
|
|
36
|
+
```bash
|
|
37
|
+
bundle exec rake rbs:list
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Check syntax of RBS files:
|
|
41
|
+
```bash
|
|
42
|
+
bundle exec rake rbs:parse
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## File Structure
|
|
46
|
+
|
|
47
|
+
The signatures mirror the structure of the `lib/` directory:
|
|
48
|
+
|
|
49
|
+
- `sig/zxcvbn.rbs` - Main Zxcvbn module
|
|
50
|
+
- `sig/zxcvbn/*.rbs` - Core classes (Tester, Score, Match, etc.)
|
|
51
|
+
- `sig/zxcvbn/matchers/*.rbs` - Pattern matcher classes
|
|
52
|
+
|
|
53
|
+
## Adding New Signatures
|
|
54
|
+
|
|
55
|
+
When adding new classes or methods to the codebase, remember to:
|
|
56
|
+
|
|
57
|
+
1. Create or update the corresponding `.rbs` file in the `sig/` directory
|
|
58
|
+
2. Run `bundle exec rake rbs_validate` to ensure the syntax is correct
|
|
59
|
+
3. Keep type signatures in sync with the actual implementation
|
|
60
|
+
|
|
61
|
+
## Resources
|
|
62
|
+
|
|
63
|
+
- [RBS Documentation](https://github.com/ruby/rbs)
|
|
64
|
+
- [RBS Syntax Guide](https://github.com/ruby/rbs/blob/master/docs/syntax.md)
|
|
65
|
+
- [Ruby Signature Collection](https://github.com/ruby/gem_rbs_collection)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
module CrackTime
|
|
3
|
+
SINGLE_GUESS: Float
|
|
4
|
+
NUM_ATTACKERS: Integer
|
|
5
|
+
SECONDS_PER_GUESS: Float
|
|
6
|
+
|
|
7
|
+
def entropy_to_crack_time: (Numeric entropy) -> Float
|
|
8
|
+
|
|
9
|
+
def crack_time_to_score: (Numeric seconds) -> Integer
|
|
10
|
+
|
|
11
|
+
def display_time: (Numeric seconds) -> String
|
|
12
|
+
end
|
|
13
|
+
end
|
data/sig/zxcvbn/data.rbs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Data
|
|
3
|
+
type ranked_dictionary = Hash[String, Integer]
|
|
4
|
+
type adjacency_graph = Hash[String, Array[String?]]
|
|
5
|
+
type graph_stats = Hash[String, { average_degree: Float, starting_positions: Integer }]
|
|
6
|
+
|
|
7
|
+
@ranked_dictionaries: Hash[String, ranked_dictionary]
|
|
8
|
+
@adjacency_graphs: Hash[String, adjacency_graph]
|
|
9
|
+
@dictionary_tries: Hash[String, Trie]
|
|
10
|
+
@graph_stats: graph_stats
|
|
11
|
+
|
|
12
|
+
attr_reader ranked_dictionaries: Hash[String, ranked_dictionary]
|
|
13
|
+
attr_reader adjacency_graphs: Hash[String, adjacency_graph]
|
|
14
|
+
attr_reader dictionary_tries: Hash[String, Trie]
|
|
15
|
+
attr_reader graph_stats: graph_stats
|
|
16
|
+
|
|
17
|
+
def initialize: () -> void
|
|
18
|
+
|
|
19
|
+
def add_word_list: (String name, Array[String] list) -> void
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def read_word_list: (String file) -> Array[String]
|
|
24
|
+
|
|
25
|
+
def build_tries: () -> Hash[String, Trie]
|
|
26
|
+
|
|
27
|
+
def build_trie: (ranked_dictionary ranked_dictionary) -> Trie
|
|
28
|
+
|
|
29
|
+
def compute_graph_stats: () -> graph_stats
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
module Entropy
|
|
3
|
+
include Zxcvbn::Math
|
|
4
|
+
|
|
5
|
+
def calc_entropy: (Match match) -> Float
|
|
6
|
+
|
|
7
|
+
def repeat_entropy: (Match match) -> Float
|
|
8
|
+
|
|
9
|
+
def sequence_entropy: (Match match) -> Float
|
|
10
|
+
|
|
11
|
+
def digits_entropy: (Match match) -> Float
|
|
12
|
+
|
|
13
|
+
def year_entropy: (Match? match) -> Float
|
|
14
|
+
|
|
15
|
+
def date_entropy: (Match match) -> Float
|
|
16
|
+
|
|
17
|
+
def dictionary_entropy: (Match match) -> Float
|
|
18
|
+
|
|
19
|
+
def extra_uppercase_entropy: (Match match) -> Numeric
|
|
20
|
+
|
|
21
|
+
def extra_l33t_entropy: (Match match) -> Numeric
|
|
22
|
+
|
|
23
|
+
def spatial_entropy: (Match match) -> Float
|
|
24
|
+
|
|
25
|
+
NUM_YEARS: Integer
|
|
26
|
+
NUM_MONTHS: Integer
|
|
27
|
+
NUM_DAYS: Integer
|
|
28
|
+
START_UPPER: Regexp
|
|
29
|
+
END_UPPER: Regexp
|
|
30
|
+
ALL_UPPER: Regexp
|
|
31
|
+
ALL_LOWER: Regexp
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class FeedbackGiver
|
|
3
|
+
NAME_DICTIONARIES: Array[String]
|
|
4
|
+
DEFAULT_FEEDBACK: Feedback
|
|
5
|
+
EMPTY_FEEDBACK: Feedback
|
|
6
|
+
|
|
7
|
+
def self.get_feedback: (Integer? score, Array[Match] sequence) -> Feedback
|
|
8
|
+
|
|
9
|
+
def self.get_match_feedback: (Match match, bool is_sole_match) -> Feedback?
|
|
10
|
+
|
|
11
|
+
def self.get_dictionary_match_feedback: (Match match, bool is_sole_match) -> Feedback
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Match
|
|
3
|
+
attr_accessor pattern: String?
|
|
4
|
+
attr_accessor i: Integer?
|
|
5
|
+
attr_accessor j: Integer?
|
|
6
|
+
attr_accessor token: String?
|
|
7
|
+
attr_accessor matched_word: String?
|
|
8
|
+
attr_accessor rank: Integer?
|
|
9
|
+
attr_accessor dictionary_name: String?
|
|
10
|
+
attr_accessor reversed: bool?
|
|
11
|
+
attr_accessor l33t: bool?
|
|
12
|
+
attr_accessor sub: Hash[String, String]?
|
|
13
|
+
attr_accessor sub_display: String?
|
|
14
|
+
attr_accessor l: Integer?
|
|
15
|
+
attr_accessor entropy: Numeric?
|
|
16
|
+
attr_accessor base_entropy: Numeric?
|
|
17
|
+
attr_accessor uppercase_entropy: Numeric?
|
|
18
|
+
attr_accessor l33t_entropy: Numeric?
|
|
19
|
+
attr_accessor repeated_char: String?
|
|
20
|
+
attr_accessor sequence_name: String?
|
|
21
|
+
attr_accessor sequence_space: Integer?
|
|
22
|
+
attr_accessor ascending: bool?
|
|
23
|
+
attr_accessor graph: String?
|
|
24
|
+
attr_accessor turns: Integer?
|
|
25
|
+
attr_accessor shifted_count: Integer?
|
|
26
|
+
attr_accessor shiffted_count: Integer?
|
|
27
|
+
attr_accessor year: Integer?
|
|
28
|
+
attr_accessor month: Integer?
|
|
29
|
+
attr_accessor day: Integer?
|
|
30
|
+
attr_accessor separator: String?
|
|
31
|
+
attr_accessor cardinality: Integer?
|
|
32
|
+
attr_accessor offset: Integer?
|
|
33
|
+
|
|
34
|
+
def initialize: (**untyped attributes) -> void
|
|
35
|
+
|
|
36
|
+
def to_hash: () -> Hash[String, untyped]
|
|
37
|
+
end
|
|
38
|
+
end
|
data/sig/zxcvbn/math.rbs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
module Math
|
|
3
|
+
def bruteforce_cardinality: (String password) -> Integer
|
|
4
|
+
|
|
5
|
+
def lg: (Numeric n) -> Float
|
|
6
|
+
|
|
7
|
+
def nCk: (Integer n, Integer k) -> Integer
|
|
8
|
+
|
|
9
|
+
def average_degree_for_graph: (String graph_name) -> Float
|
|
10
|
+
|
|
11
|
+
def starting_positions_for_graph: (String graph_name) -> Integer
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Omnimatch
|
|
3
|
+
@data: Data
|
|
4
|
+
@matchers: Array[untyped]
|
|
5
|
+
|
|
6
|
+
def initialize: (Data data) -> void
|
|
7
|
+
|
|
8
|
+
def matches: (String password, ?Array[String] user_inputs) -> Array[Match]
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def user_input_matchers: (Array[String] user_inputs) -> Array[untyped]
|
|
13
|
+
|
|
14
|
+
def build_matchers: () -> Array[untyped]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Score
|
|
3
|
+
attr_accessor entropy: Numeric?
|
|
4
|
+
attr_accessor crack_time: Numeric?
|
|
5
|
+
attr_accessor crack_time_display: String?
|
|
6
|
+
attr_accessor score: Integer?
|
|
7
|
+
attr_accessor pattern: String?
|
|
8
|
+
attr_accessor match_sequence: Array[Match]?
|
|
9
|
+
attr_accessor password: String?
|
|
10
|
+
attr_accessor calc_time: Float?
|
|
11
|
+
attr_accessor feedback: Feedback?
|
|
12
|
+
|
|
13
|
+
def initialize: (?entropy: Numeric?, ?crack_time: Numeric?, ?crack_time_display: String?, ?score: Integer?, ?match_sequence: Array[Match]?, ?password: String?) -> void
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Scorer
|
|
3
|
+
include Entropy
|
|
4
|
+
include CrackTime
|
|
5
|
+
|
|
6
|
+
@data: Data
|
|
7
|
+
|
|
8
|
+
attr_reader data: Data
|
|
9
|
+
|
|
10
|
+
def initialize: (Data data) -> void
|
|
11
|
+
|
|
12
|
+
def minimum_entropy_match_sequence: (String password, Array[Match] matches) -> Score
|
|
13
|
+
|
|
14
|
+
def score_for: (String password, Array[Match] match_sequence, Array[Float] up_to_k) -> Score
|
|
15
|
+
|
|
16
|
+
def pad_with_bruteforce_matches: (Array[Match] match_sequence, String password, Integer bruteforce_cardinality) -> Array[Match]
|
|
17
|
+
|
|
18
|
+
def make_bruteforce_match: (String password, Integer i, Integer j, Integer bruteforce_cardinality) -> Match
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Tester
|
|
3
|
+
@data: Data
|
|
4
|
+
|
|
5
|
+
def initialize: () -> void
|
|
6
|
+
|
|
7
|
+
def test: (String? password, ?Array[untyped] user_inputs) -> Score
|
|
8
|
+
|
|
9
|
+
def add_word_lists: (Hash[String, Array[untyped]] lists) -> void
|
|
10
|
+
|
|
11
|
+
def inspect: () -> String
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def sanitize: (Array[untyped] user_inputs) -> Array[String]
|
|
16
|
+
end
|
|
17
|
+
end
|
data/sig/zxcvbn/trie.rbs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zxcvbn
|
|
2
|
+
class Trie
|
|
3
|
+
type node = Hash[untyped, untyped]
|
|
4
|
+
|
|
5
|
+
@root: node
|
|
6
|
+
|
|
7
|
+
def initialize: () -> void
|
|
8
|
+
|
|
9
|
+
def insert: (String word, Integer rank) -> void
|
|
10
|
+
|
|
11
|
+
def search_prefixes: (String text, Integer start_pos) -> Array[[String, Integer?, Integer, Integer]]
|
|
12
|
+
end
|
|
13
|
+
end
|
data/sig/zxcvbn.rbs
ADDED
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.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Hodgkiss
|
|
@@ -52,16 +52,34 @@ files:
|
|
|
52
52
|
- lib/zxcvbn/score.rb
|
|
53
53
|
- lib/zxcvbn/scorer.rb
|
|
54
54
|
- lib/zxcvbn/tester.rb
|
|
55
|
+
- lib/zxcvbn/trie.rb
|
|
55
56
|
- lib/zxcvbn/version.rb
|
|
56
|
-
|
|
57
|
+
- sig/README.md
|
|
58
|
+
- sig/zxcvbn.rbs
|
|
59
|
+
- sig/zxcvbn/crack_time.rbs
|
|
60
|
+
- sig/zxcvbn/data.rbs
|
|
61
|
+
- sig/zxcvbn/dictionary_ranker.rbs
|
|
62
|
+
- sig/zxcvbn/entropy.rbs
|
|
63
|
+
- sig/zxcvbn/feedback.rbs
|
|
64
|
+
- sig/zxcvbn/feedback_giver.rbs
|
|
65
|
+
- sig/zxcvbn/match.rbs
|
|
66
|
+
- sig/zxcvbn/math.rbs
|
|
67
|
+
- sig/zxcvbn/omnimatch.rbs
|
|
68
|
+
- sig/zxcvbn/password_strength.rbs
|
|
69
|
+
- sig/zxcvbn/score.rbs
|
|
70
|
+
- sig/zxcvbn/scorer.rbs
|
|
71
|
+
- sig/zxcvbn/tester.rbs
|
|
72
|
+
- sig/zxcvbn/trie.rbs
|
|
73
|
+
homepage: https://github.com/envato/zxcvbn-ruby
|
|
57
74
|
licenses:
|
|
58
75
|
- MIT
|
|
59
76
|
metadata:
|
|
77
|
+
allowed_push_host: https://rubygems.org
|
|
60
78
|
bug_tracker_uri: https://github.com/envato/zxcvbn-ruby/issues
|
|
61
79
|
changelog_uri: https://github.com/envato/zxcvbn-ruby/blob/HEAD/CHANGELOG.md
|
|
62
|
-
documentation_uri: https://
|
|
80
|
+
documentation_uri: https://www.rubydoc.info/gems/zxcvbn-ruby/1.4.0
|
|
63
81
|
homepage_uri: https://github.com/envato/zxcvbn-ruby
|
|
64
|
-
source_code_uri: https://github.com/envato/zxcvbn-ruby
|
|
82
|
+
source_code_uri: https://github.com/envato/zxcvbn-ruby/tree/v1.4.0
|
|
65
83
|
rdoc_options: []
|
|
66
84
|
require_paths:
|
|
67
85
|
- lib
|
|
@@ -76,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
94
|
- !ruby/object:Gem::Version
|
|
77
95
|
version: '0'
|
|
78
96
|
requirements: []
|
|
79
|
-
rubygems_version: 4.0.
|
|
97
|
+
rubygems_version: 4.0.4
|
|
80
98
|
specification_version: 4
|
|
81
99
|
summary: ''
|
|
82
100
|
test_files: []
|