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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -1
  3. data/README.md +322 -75
  4. data/data/frequency_lists/english_wikipedia.txt +30000 -0
  5. data/data/frequency_lists/female_names.txt +11 -114
  6. data/data/frequency_lists/male_names.txt +3 -24
  7. data/data/frequency_lists/passwords.txt +29623 -6764
  8. data/data/frequency_lists/surnames.txt +28 -30611
  9. data/data/frequency_lists/{english.txt → us_tv_and_film.txt} +147 -13532
  10. data/lib/zxcvbn/clock.rb +6 -0
  11. data/lib/zxcvbn/crack_time.rb +52 -18
  12. data/lib/zxcvbn/data.rb +61 -21
  13. data/lib/zxcvbn/dictionary_ranker.rb +10 -0
  14. data/lib/zxcvbn/feedback.rb +11 -6
  15. data/lib/zxcvbn/feedback_giver.rb +75 -50
  16. data/lib/zxcvbn/guesses.rb +208 -0
  17. data/lib/zxcvbn/match.rb +95 -15
  18. data/lib/zxcvbn/match_builder.rb +15 -0
  19. data/lib/zxcvbn/matchers/date.rb +171 -106
  20. data/lib/zxcvbn/matchers/dictionary.rb +15 -8
  21. data/lib/zxcvbn/matchers/digits.rb +6 -1
  22. data/lib/zxcvbn/matchers/l33t.rb +30 -34
  23. data/lib/zxcvbn/matchers/regex_helpers.rb +14 -6
  24. data/lib/zxcvbn/matchers/repeat.rb +47 -16
  25. data/lib/zxcvbn/matchers/sequences.rb +58 -48
  26. data/lib/zxcvbn/matchers/spatial.rb +22 -6
  27. data/lib/zxcvbn/matchers/year.rb +6 -1
  28. data/lib/zxcvbn/math.rb +15 -28
  29. data/lib/zxcvbn/omnimatch.rb +70 -22
  30. data/lib/zxcvbn/ruby.rb +3 -0
  31. data/lib/zxcvbn/score.rb +34 -10
  32. data/lib/zxcvbn/scorer.rb +142 -75
  33. data/lib/zxcvbn/tester.rb +58 -23
  34. data/lib/zxcvbn/tester_builder.rb +83 -0
  35. data/lib/zxcvbn/trie.rb +21 -0
  36. data/lib/zxcvbn/version.rb +1 -1
  37. data/lib/zxcvbn.rb +47 -7
  38. data/sig/README.md +65 -0
  39. data/sig/zxcvbn/clock.rbs +5 -0
  40. data/sig/zxcvbn/crack_time.rbs +11 -0
  41. data/sig/zxcvbn/data.rbs +40 -0
  42. data/sig/zxcvbn/dictionary_ranker.rbs +7 -0
  43. data/sig/zxcvbn/feedback.rbs +10 -0
  44. data/sig/zxcvbn/feedback_giver.rbs +13 -0
  45. data/sig/zxcvbn/guesses.rbs +36 -0
  46. data/sig/zxcvbn/match.rbs +40 -0
  47. data/sig/zxcvbn/match_builder.rbs +36 -0
  48. data/sig/zxcvbn/matchers/date.rbs +23 -0
  49. data/sig/zxcvbn/matchers/dictionary.rbs +21 -0
  50. data/sig/zxcvbn/matchers/digits.rbs +11 -0
  51. data/sig/zxcvbn/matchers/l33t.rbs +27 -0
  52. data/sig/zxcvbn/matchers/regex_helpers.rbs +7 -0
  53. data/sig/zxcvbn/matchers/repeat.rbs +11 -0
  54. data/sig/zxcvbn/matchers/sequences.rbs +16 -0
  55. data/sig/zxcvbn/matchers/spatial.rbs +15 -0
  56. data/sig/zxcvbn/matchers/year.rbs +11 -0
  57. data/sig/zxcvbn/math.rbs +9 -0
  58. data/sig/zxcvbn/omnimatch.rbs +19 -0
  59. data/sig/zxcvbn/score.rbs +26 -0
  60. data/sig/zxcvbn/scorer.rbs +19 -0
  61. data/sig/zxcvbn/tester.rbs +15 -0
  62. data/sig/zxcvbn/tester_builder.rbs +16 -0
  63. data/sig/zxcvbn/trie.rbs +17 -0
  64. data/sig/zxcvbn.rbs +12 -0
  65. metadata +46 -12
  66. data/lib/zxcvbn/entropy.rb +0 -158
  67. data/lib/zxcvbn/matchers/new_l33t.rb +0 -118
  68. data/lib/zxcvbn/password_strength.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36aa566fe4268e91239c2232628e3eb7397cdf06c99608efa63670842a5b5b4c
4
- data.tar.gz: 8c736d0a2f84507600e9ba3da51a368f056c2e8918672238415f636bffcd6ca3
3
+ metadata.gz: da4a05d3abd0061434bb0bcedfa1992a9e975c479a0fa9679f9b7c139ea1fa1a
4
+ data.tar.gz: c7a0a8aa222f29b4550e636701e23069599497290c0fb23edddfc7dbfc37fec5
5
5
  SHA512:
6
- metadata.gz: f569dc2cee3a3eee7c8b1adad5f924d74c0b5d2aac11eb61ec4e3330f3043f89d5543a74f073b508b0820bde71e0473b7e096c4d10138fb459fd90dcd7461667
7
- data.tar.gz: f5873e8cdf377c6e30097025c5e8b3c02a4a8c65da2fd2051ef14791ee0123a8224e1c1627a0559338e1e16aa6808e1691afd3d00515057ba79732c62737247a
6
+ metadata.gz: 5fcabd8ce5ebe85f5127225c27f1fea83bf26c80885e7f4470d037f8d2c35bda07c8886f185627ee1fdf99feabb7d29ccafba80322257b1724b2c6fc7fc1352a
7
+ data.tar.gz: 559d0b16b68a68890e898992ef11c2e45c499ab0772d928313a3ab01767c8c251002e136b80eb7ac90d8155ea7e2432d9fb6bae1c47f0d3876b8f623a1bde9bc
data/CHANGELOG.md CHANGED
@@ -6,7 +6,77 @@ 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.3.0...HEAD
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
68
+
69
+ ## [1.4.0] - 2026-01-15
70
+
71
+ ### Added
72
+ - RBS type signatures for improved type checking and IDE support ([#68])
73
+
74
+ ### Changed
75
+ - Minor fixups in gem metadata ([#67]).
76
+
77
+ [1.4.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.3.0...v1.4.0
78
+ [#67]: https://github.com/envato/zxcvbn-ruby/pull/67
79
+ [#68]: https://github.com/envato/zxcvbn-ruby/pull/68
10
80
 
11
81
  ## [1.3.0] - 2026-01-02
12
82
 
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 library.
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 [![CI Status](https://github.com/envato/zxcvbn-ruby/workflows/CI/badge.svg)](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:0x00007f7f590610c8
36
- @calc_time=0.0055760000250302255,
37
- @crack_time=0.012,
38
- @crack_time_display="instant",
39
- @entropy=7.895,
40
- @feedback=
41
- #<Zxcvbn::Feedback:0x00007f7f59060150
42
- @suggestions=
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:0x00007f7f5a9e9248
54
- @calc_time=0.007504999986849725,
55
- @crack_time=46159.451,
56
- @crack_time_display="14 hours",
57
- @entropy=29.782,
58
- @feedback=
59
- #<Zxcvbn::Feedback:0x00007f7f5a9e9130
60
- @suggestions=
61
- ["Add another word or two. Uncommon words are better.",
62
- "Use a longer keyboard pattern with more turns"],
63
- @warning="Short keyboard patterns are easy to guess">,
64
- @match_sequence=
65
- [#<Zxcvbn::Match pattern="spatial", i=0, j=15, token="asdfghju7654rewq", graph="qwerty", turns=5, shifted_count=0, entropy=29.7820508329166>],
66
- @password="asdfghju7654rewq",
67
- @score=2>
68
- => #<Zxcvbn::Score:0x00007f7f5a9e9248>
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
- ## Testing Multiple Passwords
110
+ ## Custom Testers
72
111
 
73
- The dictionaries used for password strength testing are loaded each request to `Zxcvbn.test`. If you you'd prefer to persist the dictionaries in memory (approx 20MB RSS) to perform lots of password tests in succession then you can use the `Zxcvbn::Tester` API:
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::Tester.new
118
+ >> tester = Zxcvbn.tester_builder.build
80
119
  => #<Zxcvbn::Tester:0x3fe99d869aa4>
81
120
  >> pp tester.test('@lfred2004', ['alfred'])
82
- #<Zxcvbn::Score:0x00007f7f586fcf50
83
- @calc_time=0.00631899997824803,
84
- @crack_time=0.012,
85
- @crack_time_display="instant",
86
- @entropy=7.895,
87
- @feedback=
88
- #<Zxcvbn::Feedback:0x00007f7f586fcac8
89
- @suggestions=
90
- ["Add another word or two. Uncommon words are better.",
91
- "Predictable substitutions like '@' instead of 'a' don't help very much"],
92
- @warning=nil>,
93
- @match_sequence=
94
- [#<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>,
95
- #<Zxcvbn::Match i=6, j=9, token="2004", pattern="year", entropy=6.894817763307944>],
96
- @password="@lfred2004",
97
- @score=0>
98
- => #<Zxcvbn::Score:0x00007f7f586fcf50>
99
- >> pp tester.test('@lfred2004', ['alfred'])
100
- #<Zxcvbn::Score:0x00007f7f56d57438
101
- @calc_time=0.001986999996006489,
102
- @crack_time=0.012,
103
- @crack_time_display="instant",
104
- @entropy=7.895,
105
- @feedback=
106
- #<Zxcvbn::Feedback:0x00007f7f56d56bf0
107
- @suggestions=
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
- @warning=nil>,
111
- @match_sequence=
112
- [#<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>,
113
- #<Zxcvbn::Match i=6, j=9, token="2004", pattern="year", entropy=6.894817763307944>],
114
- @password="@lfred2004",
115
- @score=0>
116
- => #<Zxcvbn::Score:0x00007f7f56d57438>
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
- **Note**: Storing the entropy of an encrypted or hashed value provides
120
- information that can make cracking the value orders of magnitude easier for an
121
- attacker. For this reason we advise you not to store the results of
122
- `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/).
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
- - [_et al._](https://github.com/envato/zxcvbn-ruby/graphs/contributors)
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 [![license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/envato/zxcvbn-ruby/blob/HEAD/LICENSE.txt)
136
383