zxcvbn-ruby 1.4.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 +59 -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/zxcvbn/clock.rbs +5 -0
- data/sig/zxcvbn/crack_time.rbs +3 -5
- data/sig/zxcvbn/data.rbs +17 -8
- data/sig/zxcvbn/feedback.rbs +6 -4
- data/sig/zxcvbn/guesses.rbs +36 -0
- data/sig/zxcvbn/match.rbs +35 -33
- 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 +0 -4
- data/sig/zxcvbn/omnimatch.rbs +5 -2
- data/sig/zxcvbn/score.rbs +22 -11
- data/sig/zxcvbn/scorer.rbs +7 -8
- data/sig/zxcvbn/tester.rbs +5 -7
- data/sig/zxcvbn/tester_builder.rbs +16 -0
- data/sig/zxcvbn/trie.rbs +4 -0
- data/sig/zxcvbn.rbs +6 -4
- metadata +30 -13
- data/lib/zxcvbn/entropy.rb +0 -158
- data/lib/zxcvbn/matchers/new_l33t.rb +0 -118
- data/lib/zxcvbn/password_strength.rb +0 -27
- data/sig/zxcvbn/entropy.rbs +0 -33
- data/sig/zxcvbn/password_strength.rbs +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da4a05d3abd0061434bb0bcedfa1992a9e975c479a0fa9679f9b7c139ea1fa1a
|
|
4
|
+
data.tar.gz: c7a0a8aa222f29b4550e636701e23069599497290c0fb23edddfc7dbfc37fec5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fcabd8ce5ebe85f5127225c27f1fea83bf26c80885e7f4470d037f8d2c35bda07c8886f185627ee1fdf99feabb7d29ccafba80322257b1724b2c6fc7fc1352a
|
|
7
|
+
data.tar.gz: 559d0b16b68a68890e898992ef11c2e45c499ab0772d928313a3ab01767c8c251002e136b80eb7ac90d8155ea7e2432d9fb6bae1c47f0d3876b8f623a1bde9bc
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,65 @@ 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/
|
|
9
|
+
[Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v2.0.0...HEAD
|
|
10
|
+
|
|
11
|
+
## [2.0.0] - 2026-05-28
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- User-input dictionary matchers now use the Trie path, preventing O(n²) slowdowns on long passwords ([#89])
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `Zxcvbn::TesterBuilder`: fluent builder for constructing a `Tester` with custom word lists and options. Obtain a builder via `Zxcvbn.tester_builder`, then chain `add_word_list`, `max_password_length`, and `build`.
|
|
18
|
+
- `Zxcvbn::Guesses` module with per-pattern guess estimation formulas matching zxcvbn.js v4: bruteforce, dictionary (with uppercase and l33t variation multipliers), spatial, repeat, sequence, digits, year, and date ([#69])
|
|
19
|
+
- `us_tv_and_film` frequency list (19,160 entries) introduced in zxcvbn.js v4 ([#69])
|
|
20
|
+
- Reverse dictionary matching in `Omnimatch` so reversed words (e.g. "drowssap") are detected and scored ([#69])
|
|
21
|
+
- `guesses` and `guesses_log10` fields on `Zxcvbn::Score` ([#69])
|
|
22
|
+
- `guesses`, `guesses_log10`, `base_token`, `repeat_count`, and `base_guesses` fields on `Zxcvbn::Match` ([#69])
|
|
23
|
+
- YARD documentation for all public classes, modules, and methods ([#72])
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **Breaking**: `Zxcvbn::Tester.new` now requires `data:` and `max_password_length:` keyword arguments with no defaults. Use `Zxcvbn.tester_builder.build` to construct a `Tester`.
|
|
27
|
+
- **Breaking**: English frequency list dictionary name renamed from `english` to `english_wikipedia`, matching the zxcvbn.js source. Affects `match.dictionary_name` in results ([#90])
|
|
28
|
+
- `Zxcvbn.test` now reuses a shared `Tester` instance across calls, avoiding repeated dictionary parsing ([#80])
|
|
29
|
+
- Repeat base tokens are now scored without `user_inputs`, matching zxcvbn.js v4. Previously, user-supplied words were propagated into the recursive scoring of a repeat's base token, causing repeat matches of user-supplied words to score lower than JS would report ([#83])
|
|
30
|
+
- `Zxcvbn::Score` is now an immutable value object backed by Ruby's `Data`. Attribute setters (`calc_time=`, `feedback=`, etc.) have been removed. Instances now support structural equality (`==`/`eql?`/`hash`) and the `with` method for creating modified copies ([#74])
|
|
31
|
+
- **Breaking**: `Zxcvbn::Match` is now an immutable value object backed by Ruby's `Data`. Attribute setters have been removed. Instances now support structural equality (`==`/`eql?`/`hash`) and the `with` method for creating modified copies ([#92])
|
|
32
|
+
- **Breaking**: `Match#to_hash` has been removed. Use `match.to_h` instead — note the shape differs: keys are symbols (not strings), all 28 attributes are included (not just those that were set), and order follows the member definition rather than being sorted alphabetically. To replicate the old behaviour: `match.to_h.transform_keys(&:to_s).compact.sort.to_h`. Additionally, `to_hash` was Ruby's implicit-conversion hook, so any code that splatted a match (`**match`) or passed it to `Hash()` will now raise `TypeError` — use `match.to_h` explicitly instead ([#92])
|
|
33
|
+
- **Breaking**: `Zxcvbn::Feedback` is now an immutable value object backed by Ruby's `Data`. Attribute setters (`warning=`, `suggestions=`) have been removed. Instances now support structural equality (`==`/`eql?`/`hash`) and the `with` method for creating modified copies ([#77])
|
|
34
|
+
- **Breaking**: Scoring algorithm aligned with zxcvbn.js v4.4.2. The dynamic programming step now minimises total guesses (`factorial(l) × cumulative_product + MIN_GUESSES^(l-1)` penalty) instead of entropy bits. Scores for many passwords will change ([#69])
|
|
35
|
+
- **Breaking**: Bruteforce cardinality is now fixed at 10 (digits only), matching zxcvbn.js v4. Previously it was computed dynamically from the character classes present in the password (10–95), so bruteforce guesses for passwords containing letters or symbols will change ([#69])
|
|
36
|
+
- **Breaking**: `Repeat` matcher now detects multi-character repeating units (e.g. `abcabc`). The `base_token` field holds the repeating unit (which may be more than one character); `repeated_char` has been removed ([#69])
|
|
37
|
+
- **Breaking**: `Match#entropy`, `Match#base_entropy`, `Match#uppercase_entropy`, and `Match#l33t_entropy` have been removed. Use `Match#guesses` and `Match#guesses_log10` instead ([#69])
|
|
38
|
+
- `crack_time_to_score` replaced by `guesses_to_score` with v4 thresholds: 0 (<1,005 guesses), 1 (<1,000,005), 2 (<100,000,005), 3 (<10,000,000,005), 4 (≥10,000,000,005) ([#69])
|
|
39
|
+
- `Sequence` matcher ported to the zxcvbn.js v4 delta-based algorithm. Sequences are now detected using codepoint deltas up to ±5 (was ±1 only), enabling matches like `"ace"` (delta 2) or Unicode runs like `"αβγ"`. Sequence type is classified by character class (`lower`/`upper`/`digits`/`unicode`) rather than a lookup table ([#69])
|
|
40
|
+
- `Date` matcher year range extended to 1000–2050; 2-digit years are now expanded (>50 → 1900s, ≤50 → 2000s) ([#69])
|
|
41
|
+
- All frequency lists replaced with zxcvbn.js v4.4.2 versions: `passwords` (30k entries), `surnames` (10k), `female_names` (3,712), `male_names` (983), `english_wikipedia` (30k entries) ([#69])
|
|
42
|
+
- **Breaking**: `entropy` on `Zxcvbn::Score` has been removed. Use `Score#guesses` or `Math.log2(score.guesses)` instead ([#69])
|
|
43
|
+
- **Breaking**: `Score#crack_time` and `Score#crack_time_display` replaced by `Score#crack_times_seconds` and `Score#crack_times_display`, each a hash keyed by attack scenario (`online_throttling_100_per_hour`, `online_no_throttling_10_per_second`, `offline_slow_hashing_1e4_per_second`, `offline_fast_hashing_1e10_per_second`), matching the zxcvbn.js v4 output format ([#69])
|
|
44
|
+
- **Breaking**: `Score#match_sequence` renamed to `Score#sequence` to match the zxcvbn.js v4 field name ([#69])
|
|
45
|
+
- **Breaking**: `Feedback#warning` now returns `''` instead of `nil` when no warning applies, matching zxcvbn.js v4 ([#69])
|
|
46
|
+
- **Breaking**: The "This is similar to a commonly used password" warning is now only emitted when `match.guesses_log10 <= 4`, matching the zxcvbn.js v4 threshold. Previously it was emitted unconditionally for any l33t, reversed, or non-sole-match on the passwords dictionary ([#69])
|
|
47
|
+
- Repeat feedback now distinguishes single-char repeats (`"aaa"`) from multi-char repeats (`"abcabcabc"`), matching zxcvbn.js v4 ([#69])
|
|
48
|
+
- `year` pattern matches now produce a "Recent years are easy to guess" warning ([#69])
|
|
49
|
+
- Sole matches from the `english_wikipedia` dictionary now produce an "A word by itself is easy to guess" warning ([#69])
|
|
50
|
+
- **Breaking**: `Tester#test` and `Zxcvbn.test` now raise `Zxcvbn::PasswordTooLong` (a subclass of `ArgumentError`) for passwords longer than 256 characters (the default). Previously, long passwords were accepted and could cause super-quadratic runtime on adversarial repeat inputs to the `password` argument. The `user_inputs` parameter remains unbounded. Override the limit with the `ZXCVBN_MAX_PASSWORD_LENGTH` environment variable or `Zxcvbn.tester_builder.max_password_length(n).build`.
|
|
51
|
+
|
|
52
|
+
### Removed
|
|
53
|
+
- **Breaking**: `Tester#add_word_lists`. Use `Zxcvbn.tester_builder.add_word_list(name, words).build` instead.
|
|
54
|
+
- **Breaking**: `word_lists:` argument to `Zxcvbn.test`. Use `Zxcvbn.tester_builder.add_word_list(name, words).build` to construct a tester with custom word lists.
|
|
55
|
+
- Support for Ruby versions below 3.3 ([#70])
|
|
56
|
+
|
|
57
|
+
[2.0.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.4.0...v2.0.0
|
|
58
|
+
[#69]: https://github.com/envato/zxcvbn-ruby/pull/69
|
|
59
|
+
[#70]: https://github.com/envato/zxcvbn-ruby/pull/70
|
|
60
|
+
[#72]: https://github.com/envato/zxcvbn-ruby/pull/72
|
|
61
|
+
[#74]: https://github.com/envato/zxcvbn-ruby/pull/74
|
|
62
|
+
[#77]: https://github.com/envato/zxcvbn-ruby/pull/77
|
|
63
|
+
[#80]: https://github.com/envato/zxcvbn-ruby/pull/80
|
|
64
|
+
[#83]: https://github.com/envato/zxcvbn-ruby/pull/83
|
|
65
|
+
[#89]: https://github.com/envato/zxcvbn-ruby/pull/89
|
|
66
|
+
[#90]: https://github.com/envato/zxcvbn-ruby/pull/90
|
|
67
|
+
[#92]: https://github.com/envato/zxcvbn-ruby/pull/92
|
|
10
68
|
|
|
11
69
|
## [1.4.0] - 2026-01-15
|
|
12
70
|
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# zxcvbn-ruby
|
|
2
2
|
|
|
3
|
-
This is a Ruby port of Dropbox's [zxcvbn.js][zxcvbn.js] JavaScript
|
|
3
|
+
This is a Ruby port of Dropbox's [zxcvbn.js][zxcvbn.js] JavaScript password strength estimator, providing **zxcvbn.js v4** compatibility.
|
|
4
4
|
|
|
5
5
|
## Development status [](https://github.com/envato/zxcvbn-ruby/actions?query=workflow%3ACI)
|
|
6
6
|
|
|
@@ -32,94 +32,340 @@ $ irb
|
|
|
32
32
|
>> require 'zxcvbn'
|
|
33
33
|
=> true
|
|
34
34
|
>> pp Zxcvbn.test('@lfred2004', ['alfred'])
|
|
35
|
-
#<Zxcvbn::Score
|
|
36
|
-
@
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
#<data Zxcvbn::Score
|
|
36
|
+
password="@lfred2004",
|
|
37
|
+
guesses=15000.0,
|
|
38
|
+
sequence=
|
|
39
|
+
[#<data Zxcvbn::Match
|
|
40
|
+
pattern="dictionary",
|
|
41
|
+
i=0,
|
|
42
|
+
j=5,
|
|
43
|
+
token="@lfred",
|
|
44
|
+
matched_word="alfred",
|
|
45
|
+
rank=1,
|
|
46
|
+
dictionary_name="user_inputs",
|
|
47
|
+
l33t=true,
|
|
48
|
+
sub={"@" => "a"},
|
|
49
|
+
sub_display="@ -> a",
|
|
50
|
+
guesses=50,
|
|
51
|
+
guesses_log10=1.6989700043360187,
|
|
52
|
+
base_guesses=1,
|
|
53
|
+
uppercase_variations=1,
|
|
54
|
+
l33t_variations=2>,
|
|
55
|
+
#<data Zxcvbn::Match
|
|
56
|
+
pattern="year",
|
|
57
|
+
i=6,
|
|
58
|
+
j=9,
|
|
59
|
+
token="2004",
|
|
60
|
+
guesses=50,
|
|
61
|
+
guesses_log10=1.6989700043360187>],
|
|
62
|
+
crack_times_seconds=
|
|
63
|
+
{"online_throttling_100_per_hour" => 540000.0,
|
|
64
|
+
"online_no_throttling_10_per_second" => 1500.0,
|
|
65
|
+
"offline_slow_hashing_1e4_per_second" => 1.5,
|
|
66
|
+
"offline_fast_hashing_1e10_per_second" => 1.5e-06},
|
|
67
|
+
crack_times_display=
|
|
68
|
+
{"online_throttling_100_per_hour" => "6 days",
|
|
69
|
+
"online_no_throttling_10_per_second" => "25 minutes",
|
|
70
|
+
"offline_slow_hashing_1e4_per_second" => "2 seconds",
|
|
71
|
+
"offline_fast_hashing_1e10_per_second" => "less than a second"},
|
|
72
|
+
score=1,
|
|
73
|
+
calc_time=0.0007990000303834677,
|
|
74
|
+
feedback=
|
|
75
|
+
#<data Zxcvbn::Feedback
|
|
76
|
+
warning="",
|
|
77
|
+
suggestions=
|
|
43
78
|
["Add another word or two. Uncommon words are better.",
|
|
44
|
-
"Predictable substitutions like '@' instead of 'a' don't help very much"]
|
|
45
|
-
@warning=nil>,
|
|
46
|
-
@match_sequence=
|
|
47
|
-
[#<Zxcvbn::Match matched_word="alfred", token="@lfred", i=0, j=5, rank=1, pattern="dictionary", dictionary_name="user_inputs", l33t=true, sub={"@"=>"a"}, sub_display="@ -> a", base_entropy=0.0, uppercase_entropy=0.0, l33t_entropy=1, entropy=1.0>,
|
|
48
|
-
#<Zxcvbn::Match i=6, j=9, token="2004", pattern="year", entropy=6.894817763307944>],
|
|
49
|
-
@password="@lfred2004",
|
|
50
|
-
@score=0>
|
|
51
|
-
=> #<Zxcvbn::Score:0x00007f7f59060150>
|
|
79
|
+
"Predictable substitutions like '@' instead of 'a' don't help very much"]>>
|
|
52
80
|
>> pp Zxcvbn.test('asdfghju7654rewq', ['alfred'])
|
|
53
|
-
#<Zxcvbn::Score
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
=>
|
|
81
|
+
#<data Zxcvbn::Score
|
|
82
|
+
password="asdfghju7654rewq",
|
|
83
|
+
guesses=923189026.4430684,
|
|
84
|
+
sequence=
|
|
85
|
+
[#<data Zxcvbn::Match
|
|
86
|
+
pattern="spatial",
|
|
87
|
+
i=0,
|
|
88
|
+
j=15,
|
|
89
|
+
token="asdfghju7654rewq",
|
|
90
|
+
guesses=923189025.4430684,
|
|
91
|
+
guesses_log10=8.965290633097352,
|
|
92
|
+
graph="qwerty",
|
|
93
|
+
turns=5,
|
|
94
|
+
shifted_count=0>],
|
|
95
|
+
crack_times_seconds=
|
|
96
|
+
{"online_throttling_100_per_hour" => 33234804951.950462,
|
|
97
|
+
"online_no_throttling_10_per_second" => 92318902.64430684,
|
|
98
|
+
"offline_slow_hashing_1e4_per_second" => 92318.90264430684,
|
|
99
|
+
"offline_fast_hashing_1e10_per_second" => 0.09231890264430684},
|
|
100
|
+
crack_times_display=
|
|
101
|
+
{"online_throttling_100_per_hour" => "centuries",
|
|
102
|
+
"online_no_throttling_10_per_second" => "3 years",
|
|
103
|
+
"offline_slow_hashing_1e4_per_second" => "1 day",
|
|
104
|
+
"offline_fast_hashing_1e10_per_second" => "less than a second"},
|
|
105
|
+
score=3,
|
|
106
|
+
calc_time=0.001090999983716756,
|
|
107
|
+
feedback=#<data Zxcvbn::Feedback warning="" suggestions=[]>>
|
|
69
108
|
```
|
|
70
109
|
|
|
71
|
-
##
|
|
110
|
+
## Custom Testers
|
|
72
111
|
|
|
73
|
-
|
|
112
|
+
`Zxcvbn.test` reuses a shared `Tester` internally — dictionaries are loaded once and persist in memory. Use `Zxcvbn.tester_builder.build` to construct a standalone `Tester` you control:
|
|
74
113
|
|
|
75
114
|
```ruby
|
|
76
115
|
$ irb
|
|
77
116
|
>> require 'zxcvbn'
|
|
78
117
|
=> true
|
|
79
|
-
>> tester = Zxcvbn
|
|
118
|
+
>> tester = Zxcvbn.tester_builder.build
|
|
80
119
|
=> #<Zxcvbn::Tester:0x3fe99d869aa4>
|
|
81
120
|
>> pp tester.test('@lfred2004', ['alfred'])
|
|
82
|
-
#<Zxcvbn::Score
|
|
83
|
-
@
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
#<data Zxcvbn::Score
|
|
122
|
+
password="@lfred2004",
|
|
123
|
+
guesses=15000.0,
|
|
124
|
+
sequence=
|
|
125
|
+
[#<data Zxcvbn::Match
|
|
126
|
+
pattern="dictionary",
|
|
127
|
+
i=0,
|
|
128
|
+
j=5,
|
|
129
|
+
token="@lfred",
|
|
130
|
+
matched_word="alfred",
|
|
131
|
+
rank=1,
|
|
132
|
+
dictionary_name="user_inputs",
|
|
133
|
+
l33t=true,
|
|
134
|
+
sub={"@" => "a"},
|
|
135
|
+
sub_display="@ -> a",
|
|
136
|
+
guesses=50,
|
|
137
|
+
guesses_log10=1.6989700043360187,
|
|
138
|
+
base_guesses=1,
|
|
139
|
+
uppercase_variations=1,
|
|
140
|
+
l33t_variations=2>,
|
|
141
|
+
#<data Zxcvbn::Match
|
|
142
|
+
pattern="year",
|
|
143
|
+
i=6,
|
|
144
|
+
j=9,
|
|
145
|
+
token="2004",
|
|
146
|
+
guesses=50,
|
|
147
|
+
guesses_log10=1.6989700043360187>],
|
|
148
|
+
crack_times_seconds=
|
|
149
|
+
{"online_throttling_100_per_hour" => 540000.0,
|
|
150
|
+
"online_no_throttling_10_per_second" => 1500.0,
|
|
151
|
+
"offline_slow_hashing_1e4_per_second" => 1.5,
|
|
152
|
+
"offline_fast_hashing_1e10_per_second" => 1.5e-06},
|
|
153
|
+
crack_times_display=
|
|
154
|
+
{"online_throttling_100_per_hour" => "6 days",
|
|
155
|
+
"online_no_throttling_10_per_second" => "25 minutes",
|
|
156
|
+
"offline_slow_hashing_1e4_per_second" => "2 seconds",
|
|
157
|
+
"offline_fast_hashing_1e10_per_second" => "less than a second"},
|
|
158
|
+
score=1,
|
|
159
|
+
calc_time=0.0008110000053420663,
|
|
160
|
+
feedback=
|
|
161
|
+
#<data Zxcvbn::Feedback
|
|
162
|
+
warning="",
|
|
163
|
+
suggestions=
|
|
108
164
|
["Add another word or two. Uncommon words are better.",
|
|
109
|
-
"Predictable substitutions like '@' instead of 'a' don't help very much"]
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
=> #<Zxcvbn::
|
|
165
|
+
"Predictable substitutions like '@' instead of 'a' don't help very much"]>>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
To add custom word lists, chain `add_word_list` before `build`:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
>> tester = Zxcvbn.tester_builder.add_word_list('company', %w[acme corp]).build
|
|
172
|
+
=> #<Zxcvbn::Tester:0x3fe99d869bb8>
|
|
173
|
+
>> tester.test('acme').score
|
|
174
|
+
=> 0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Subsequent calls reuse the already-loaded dictionaries, so `calc_time` is significantly lower:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
>> tester.test('@lfred2004', ['alfred']).calc_time
|
|
181
|
+
=> 0.0005759999621659517
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> [!WARNING]
|
|
185
|
+
> Scoring time grows with password length. For adversarial inputs such as
|
|
186
|
+
> short repeated sequences (e.g. `"ab" * 500`), the internal pattern-matching
|
|
187
|
+
> DP produces super-quadratic runtime. Both `Zxcvbn.test` and
|
|
188
|
+
> `Zxcvbn::Tester#test` raise `Zxcvbn::PasswordTooLong` for passwords longer
|
|
189
|
+
> than 256 characters (the default). Override the
|
|
190
|
+
> limit with the `ZXCVBN_MAX_PASSWORD_LENGTH` environment variable, but be
|
|
191
|
+
> aware that raising it re-exposes this runtime risk. The `user_inputs`
|
|
192
|
+
> parameter is not length-bounded; apply your own limit to those values.
|
|
193
|
+
|
|
194
|
+
> [!WARNING]
|
|
195
|
+
> Storing the guesses or score for an encrypted or hashed value provides
|
|
196
|
+
> information that can make cracking the value orders of magnitude easier for an
|
|
197
|
+
> attacker. For this reason we advise you not to store the results of
|
|
198
|
+
> `Zxcvbn::Tester#test`. Further reading: [A Tale of Security Gone Wrong](https://web.archive.org/web/20240715041147/http://gavinmiller.io/2016/a-tale-of-security-gone-wrong/).
|
|
199
|
+
|
|
200
|
+
## Limitations
|
|
201
|
+
|
|
202
|
+
The frequency lists bundled with this gem are English-only: English Wikipedia, US TV & film, English-derived names, and English-language password leaks. Passwords built from non-English words (e.g. `"รหัสผ่าน"`, Thai for "password") are scored against bruteforce rather than a localised dictionary, so their score will often be higher than the real-world risk warrants. If your users primarily choose passwords in a non-English language, treat the scores as a lower bound on strength, not an absolute measure.
|
|
203
|
+
|
|
204
|
+
## Migrating from 1.x
|
|
205
|
+
|
|
206
|
+
Version 2 aligns with the zxcvbn.js v4 algorithm and API. Scores will change for many passwords — this is expected. The sections below cover every breaking API change.
|
|
207
|
+
|
|
208
|
+
### Ruby version
|
|
209
|
+
|
|
210
|
+
Ruby 3.3 or later is required.
|
|
211
|
+
|
|
212
|
+
### `Score` field changes
|
|
213
|
+
|
|
214
|
+
The following attributes have been removed and will raise `NoMethodError`. Use the 2.x equivalents below:
|
|
215
|
+
|
|
216
|
+
| Removed (1.x) | 2.x equivalent |
|
|
217
|
+
|-----|-----|
|
|
218
|
+
| `result.entropy` | `Math.log2(result.guesses)` or `result.guesses_log10 * Math.log2(10)` |
|
|
219
|
+
| `result.crack_time` | `result.crack_times_seconds["online_no_throttling_10_per_second"]` (see [Attack scenarios](#attack-scenarios)) |
|
|
220
|
+
| `result.crack_time_display` | `result.crack_times_display["online_no_throttling_10_per_second"]` |
|
|
221
|
+
| `result.match_sequence` | `result.sequence` |
|
|
222
|
+
|
|
223
|
+
1.x `crack_time` used 10 guesses/second (unthrottled online), corresponding to the `"online_no_throttling_10_per_second"` scenario. The entropy formula gives the same log-scale difficulty value, but the number will differ from 1.x because the underlying guessing algorithm has been rewritten.
|
|
224
|
+
|
|
225
|
+
`Score` is now an immutable value object. Attribute setters (`calc_time=`, `feedback=`, etc.) have been removed. Use `result.with` to create a modified copy — `with` returns a **new** frozen object:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# 1.x
|
|
229
|
+
result.calc_time = 0.001
|
|
230
|
+
|
|
231
|
+
# 2.x
|
|
232
|
+
result = result.with(calc_time: 0.001)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Attack scenarios
|
|
236
|
+
|
|
237
|
+
`crack_times_seconds` and `crack_times_display` are both hashes keyed by attack scenario:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
result.crack_times_display
|
|
241
|
+
# => {
|
|
242
|
+
# "online_throttling_100_per_hour" => "6 days",
|
|
243
|
+
# "online_no_throttling_10_per_second" => "25 minutes",
|
|
244
|
+
# "offline_slow_hashing_1e4_per_second" => "2 seconds",
|
|
245
|
+
# "offline_fast_hashing_1e10_per_second" => "less than a second"
|
|
246
|
+
# }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### `Match` field changes
|
|
250
|
+
|
|
251
|
+
The following attributes have been removed and will raise `NoMethodError`. Use the 2.x equivalents below:
|
|
252
|
+
|
|
253
|
+
| Removed (1.x) | 2.x equivalent |
|
|
254
|
+
|-----|-----|
|
|
255
|
+
| `match.entropy` | `match.guesses_log10 * Math.log2(10)` |
|
|
256
|
+
| `match.base_entropy` | `Math.log2(match.base_guesses)` (dictionary matches only — `base_guesses` is `nil` for other patterns) |
|
|
257
|
+
| `match.uppercase_entropy` | `Math.log2(match.uppercase_variations)` (dictionary matches only — `uppercase_variations` is `nil` for other patterns) |
|
|
258
|
+
| `match.l33t_entropy` | `Math.log2(match.l33t_variations)` (dictionary matches only — `l33t_variations` is `nil` for other patterns) |
|
|
259
|
+
| `match.repeated_char` | `match.base_token` (now supports multi-character repeating units e.g. `"abcabc"`) |
|
|
260
|
+
| `match.to_hash` | `match.to_h` — note: keys are now symbols (not strings), all 28 attributes are included (not just those that were set), and key order follows member definition rather than alphabetical. To replicate old behaviour: `match.to_h.transform_keys(&:to_s).compact.sort.to_h` |
|
|
261
|
+
|
|
262
|
+
These translations give a log-scale difficulty value but are not numerically equivalent to 1.x — the underlying guess estimation formulas have been rewritten.
|
|
263
|
+
|
|
264
|
+
`Match` is now an immutable value object. Attribute setters have been removed. Use `match.with(attr: value)` to derive a modified copy. Any code that splatted a match (`**match`) or passed it to `Hash()` will now raise `TypeError` — use `match.to_h` explicitly instead.
|
|
265
|
+
|
|
266
|
+
### `Feedback` changes
|
|
267
|
+
|
|
268
|
+
`Feedback#warning` now returns `''` instead of `nil` when no warning applies. Update `nil` checks accordingly:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# 1.x
|
|
272
|
+
if result.feedback.warning
|
|
273
|
+
show_warning(result.feedback.warning)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# 2.x
|
|
277
|
+
unless result.feedback.warning.empty?
|
|
278
|
+
show_warning(result.feedback.warning)
|
|
279
|
+
end
|
|
117
280
|
```
|
|
118
281
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
282
|
+
Also update any `||` defaults on `warning` — `''` is truthy so the fallback no longer fires:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# 1.x — worked because nil is falsy
|
|
286
|
+
label = result.feedback.warning || "No issues"
|
|
287
|
+
|
|
288
|
+
# 2.x
|
|
289
|
+
label = result.feedback.warning.empty? ? "No issues" : result.feedback.warning
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The "This is similar to a commonly used password" warning is now only emitted when `match.guesses_log10 <= 4` (previously it applied to any l33t, reversed, or non-sole-match on the passwords dictionary). Update any code that asserts on feedback content.
|
|
293
|
+
|
|
294
|
+
`Feedback` is now an immutable value object. Attribute setters (`warning=`, `suggestions=`) have been removed. Use `result.feedback.with(warning: "...")` to derive a modified copy.
|
|
295
|
+
|
|
296
|
+
### Dictionary name change
|
|
297
|
+
|
|
298
|
+
The `english` frequency list has been renamed to `english_wikipedia`. If you filter matches by `dictionary_name`:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# 1.x
|
|
302
|
+
match.dictionary_name == "english"
|
|
303
|
+
|
|
304
|
+
# 2.x
|
|
305
|
+
match.dictionary_name == "english_wikipedia"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
A new `us_tv_and_film` frequency list has also been added. Update any `dictionary_name` allowlists or case statements to include it.
|
|
309
|
+
|
|
310
|
+
### Password length limit
|
|
311
|
+
|
|
312
|
+
`Tester#test` and `Zxcvbn.test` now raise `Zxcvbn::PasswordTooLong` (a subclass of `ArgumentError`) for passwords longer than 256 characters (the default). Previously, long passwords were accepted without error. The `user_inputs` parameter remains unbounded.
|
|
313
|
+
|
|
314
|
+
If your application accepts user-controlled input longer than 256 characters, either add a length check before calling the gem, construct a `Tester` with a custom limit, or adjust the process-wide default via the `ZXCVBN_MAX_PASSWORD_LENGTH` environment variable.
|
|
315
|
+
|
|
316
|
+
To enforce your own limit before calling the gem (note: bcrypt's limit is 72 **bytes**, not characters):
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
raise ArgumentError, "Password too long" if password.bytesize > 72 # bcrypt's 72-byte limit
|
|
320
|
+
result = Zxcvbn.test(password)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
To use a different limit for a specific tester without touching the environment:
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
tester = Zxcvbn.tester_builder.max_password_length(128).build
|
|
327
|
+
result = tester.test(password)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
To adjust the process-wide default, set `ZXCVBN_MAX_PASSWORD_LENGTH` in the process environment before the process starts:
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
ZXCVBN_MAX_PASSWORD_LENGTH=1024 bundle exec rails server
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The variable is read when `TesterBuilder#build` is called. For the shared tester backing `Zxcvbn.test`, that is the first call to `Zxcvbn.test`.
|
|
337
|
+
|
|
338
|
+
Or export it from your shell profile, process manager, or platform environment config (Heroku, Docker, etc.). Note that raising the limit re-exposes the super-quadratic runtime for adversarial inputs to the `password` argument.
|
|
339
|
+
|
|
340
|
+
### Custom word lists
|
|
341
|
+
|
|
342
|
+
`Tester#add_word_lists` and the `word_lists:` argument to `Zxcvbn.test` have been removed. Use the `Zxcvbn.tester_builder` builder instead:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# 1.x — no longer works
|
|
346
|
+
result = Zxcvbn.test(password, user_inputs, word_lists: { 'company' => %w[acme corp] })
|
|
347
|
+
tester.add_word_lists('company' => %w[acme corp])
|
|
348
|
+
|
|
349
|
+
# 2.x
|
|
350
|
+
tester = Zxcvbn.tester_builder.add_word_list('company', %w[acme corp]).build
|
|
351
|
+
result = tester.test(password, user_inputs)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`Zxcvbn::Tester.new` is no longer a public construction path — it now requires `data:` and `max_password_length:` keyword arguments with no defaults. Use `Zxcvbn.tester_builder.build` (or the fluent builder) instead:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# 1.x
|
|
358
|
+
tester = Zxcvbn::Tester.new
|
|
359
|
+
|
|
360
|
+
# 2.x
|
|
361
|
+
tester = Zxcvbn.tester_builder.build
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Score values will change
|
|
365
|
+
|
|
366
|
+
The scoring algorithm has been rewritten to match zxcvbn.js v4. It now minimises total guesses rather than entropy bits. Bruteforce cardinality is fixed at 10 regardless of the character classes in the password (previously 10–95 depending on which character classes were present). Scores (0–4) for many passwords will differ from 1.x — this is expected and intentional.
|
|
367
|
+
|
|
368
|
+
Audit any code that gates on `result.score` (e.g. form validation thresholds), persists scores in a database, or asserts on score values in tests — these will need review after upgrading.
|
|
123
369
|
|
|
124
370
|
## Contact
|
|
125
371
|
|
|
@@ -130,7 +376,8 @@ attacker. For this reason we advise you not to store the results of
|
|
|
130
376
|
|
|
131
377
|
- [Steve Hodgkiss](https://github.com/stevehodgkiss)
|
|
132
378
|
- [Matthieu Aussaguel](https://github.com/matthieua)
|
|
133
|
-
- [
|
|
379
|
+
- [Orien Madgwick](https://github.com/orien)
|
|
380
|
+
- [_et al._](https://github.com/envato/zxcvbn-ruby/graphs/contributors?all=1)
|
|
134
381
|
|
|
135
382
|
## License [](https://github.com/envato/zxcvbn-ruby/blob/HEAD/LICENSE.txt)
|
|
136
383
|
|