passlib 0.1.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.
data/README.md ADDED
@@ -0,0 +1,726 @@
1
+ # Passlib
2
+
3
+ A Ruby password hashing library inspired by Python's [Passlib](https://passlib.readthedocs.io). It provides a unified interface for creating and verifying password hashes across a wide range of algorithms and auto-detects the algorithm from any stored hash string, making it easy to support multiple formats or migrate between them.
4
+
5
+ Currently supported algorithms include argon2, balloon hashing, bcrypt, bcrypt-sha256, LDAP-style digests (salted and unsalted MD5, SHA-1, SHA-256, SHA-512, SHA3-256, and SHA3-512), MD5-crypt (including Apache variant), phpass, PBKDF2 (with SHA1, SHA256, SHA512, SHA3-256, and SHA3-512), scrypt, SHA1-crypt, SHA2-crypt (SHA-256 and SHA-512 variants), and yescrypt. The library is designed to be extensible, so support for additional algorithms can be added in the future.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Passlib](#passlib)
10
+ - [Table of Contents](#table-of-contents)
11
+ - [Installation](#installation)
12
+ - [Features](#features)
13
+ - [Create and verify hashes](#create-and-verify-hashes)
14
+ - [Recreating hashes](#recreating-hashes)
15
+ - [Isolated contexts](#isolated-contexts)
16
+ - [Upgrading password hashes](#upgrading-password-hashes)
17
+ - [Check algorithm availability at runtime](#check-algorithm-availability-at-runtime)
18
+ - [Integration with other tools, libraries, and frameworks](#integration-with-other-tools-libraries-and-frameworks)
19
+ - [Devise](#devise)
20
+ - [Configuration](#configuration)
21
+ - [Global configuration](#global-configuration)
22
+ - [Preferred scheme](#preferred-scheme)
23
+ - [Default setting](#default-setting)
24
+ - [Per-algorithm settings](#per-algorithm-settings)
25
+ - [Configuration inheritance](#configuration-inheritance)
26
+ - [Thread safety](#thread-safety)
27
+ - [Supported hash formats and algorithms](#supported-hash-formats-and-algorithms)
28
+ - [`argon2`](#argon2)
29
+ - [`balloon`](#balloon)
30
+ - [`bcrypt`](#bcrypt)
31
+ - [`bcrypt_sha256`](#bcrypt_sha256)
32
+ - [`ldap_digest`](#ldap_digest)
33
+ - [`md5_crypt`](#md5_crypt)
34
+ - [`pbkdf2`](#pbkdf2)
35
+ - [`phpass`](#phpass)
36
+ - [`scrypt`](#scrypt)
37
+ - [`sha1_crypt`](#sha1_crypt)
38
+ - [`sha2_crypt`](#sha2_crypt)
39
+ - [`yescrypt`](#yescrypt)
40
+ - [License](#license)
41
+
42
+ ## Installation
43
+
44
+ The `passlib` gem includes built-in support for all hash formats that only require OpenSSL (`md5_crypt`, `sha1_crypt`, `sha2_crypt`, `pbkdf2`, `ldap_digest`). Algorithms backed by an external gem are optional and loaded on demand, so you can install only the ones you need.
45
+
46
+ ```sh
47
+ gem install passlib
48
+
49
+ # Optional algorithm dependencies (install any combination):
50
+ gem install bcrypt
51
+ gem install argon2
52
+ gem install scrypt
53
+ gem install balloon_hashing
54
+ gem install yescrypt
55
+ ```
56
+
57
+ To install `passlib` together with all optional dependencies at once, use `passlib-all`:
58
+
59
+ ```sh
60
+ gem install passlib-all
61
+ ```
62
+
63
+ **With Bundler**, add only what you need:
64
+
65
+ ```ruby
66
+ # Gemfile
67
+ gem "passlib"
68
+
69
+ # Optional, add any combination:
70
+ gem "bcrypt"
71
+ gem "argon2"
72
+ gem "scrypt"
73
+ gem "balloon_hashing"
74
+ gem "yescrypt"
75
+ ```
76
+
77
+ Or pull in everything at once:
78
+
79
+ ```ruby
80
+ # Gemfile
81
+ gem "passlib-all"
82
+ ```
83
+
84
+ ## Features
85
+
86
+ ### Create and verify hashes
87
+
88
+ All hash classes share the same interface. Call `.create` to hash a new password, `.load` to parse a stored hash string, and `#verify` to verify a plaintext against a loaded hash.
89
+
90
+ ```ruby
91
+ # Hash a new password (uses the configured default algorithm)
92
+ hash = Passlib.create("hunter2")
93
+ hash.to_s # => "$argon2id$..."
94
+ hash.verify("hunter2") # => true
95
+ hash.verify("wrong") # => false
96
+
97
+ # Load a stored hash and verify
98
+ hash = Passlib.load("$argon2id$...")
99
+ hash.verify("hunter2") # => true
100
+ ```
101
+
102
+ `Passlib.load` auto-detects the algorithm from the hash string, so you can verify hashes without knowing which algorithm produced them:
103
+
104
+ ```ruby
105
+ Passlib.load("$2a$12$...").verify("hunter2") # bcrypt
106
+ Passlib.load("$argon2id$...").verify("hunter2") # argon2
107
+ Passlib.load("{SSHA512}...").verify("hunter2") # LDAP
108
+ ```
109
+
110
+ For the common case of verifying a stored hash, `Passlib.verify` combines load and match in one call:
111
+
112
+ ```ruby
113
+ Passlib.verify("hunter2", "$argon2id$...") # => true
114
+ ```
115
+
116
+ The `verify` methods are also aliased as `matches?`, `valid_password?`, `valid_secret?`, and on Password instances as `===`, which allows usage in case statements:
117
+
118
+ ```ruby
119
+ hash = Passlib.load("$argon2id$...")
120
+
121
+ case "hunter2"
122
+ when hash then puts "Password is correct"
123
+ else puts "Password is incorrect"
124
+ end
125
+ ```
126
+
127
+ ### Recreating hashes
128
+
129
+ Most algorithms also support `#create_comparable`, which re-hashes a plaintext using the same parameters (salt, rounds, etc.) as an existing hash. Not all algorithms implement it, so check with `respond_to?` before calling it directly. Note that comparing the resulting strings with `==` is vulnerable to timing attacks; use `Passlib.secure_compare` instead:
130
+
131
+ ```ruby
132
+ if hash.respond_to?(:create_comparable)
133
+ rehashed = hash.create_comparable("hunter2")
134
+ Passlib.secure_compare(rehashed, hash) # => true if passwords match
135
+ end
136
+ ```
137
+
138
+ ### Isolated contexts
139
+
140
+ `Passlib::Context` lets you create isolated configuration contexts, each with its own preferred algorithm and settings, without touching the global `Passlib` configuration. This is useful in multi-tenant applications or libraries that want to avoid interfering with the host app's configuration.
141
+
142
+ ```ruby
143
+ context = Passlib::Context.new(preferred_scheme: :bcrypt)
144
+ hash = context.create("hunter2")
145
+ hash.class # => Passlib::BCrypt
146
+ context.verify("hunter2", hash) # => true
147
+ ```
148
+
149
+ A context can inherit from another context or configuration object, so you can share a base setup and override only what you need:
150
+
151
+ ```ruby
152
+ base = Passlib::Context.new(preferred_scheme: :bcrypt)
153
+ context = Passlib::Context.new(base)
154
+ context.create("hunter2").class # => Passlib::BCrypt
155
+ ```
156
+
157
+ You can also reconfigure a context after creation using `configure`:
158
+
159
+ ```ruby
160
+ context = Passlib::Context.new
161
+ context.configure { |c| c.preferred_scheme = :bcrypt }
162
+ ```
163
+
164
+ A context exposes the same interface as the `Passlib` module.
165
+
166
+ ### Upgrading password hashes
167
+
168
+ As hashing standards evolve, stored hashes may need to be migrated to a stronger algorithm or higher cost parameters. `Passlib.upgrade?` checks whether a hash needs upgrading, and `Passlib.upgrade` performs the upgrade during login when the plaintext password is available.
169
+
170
+ ```ruby
171
+ Passlib.config.preferred_scheme = :bcrypt
172
+ Passlib.config.bcrypt.cost = 12
173
+
174
+ # Check whether a hash is outdated
175
+ Passlib.upgrade?("$2a$04$...") # => true (cost 4 is below the configured 12)
176
+ Passlib.upgrade?("$2a$12$...") # => false (already at cost 12)
177
+ Passlib.upgrade?("{SSHA512}...") # => true (wrong algorithm)
178
+ ```
179
+
180
+ `upgrade` combines verification and re-hashing in one step, returning a new hash when an upgrade is needed or `nil` when no change is required. Call it at login time and, when it returns a new hash, persist it in place of the old one:
181
+
182
+ ```ruby
183
+ def login(user, password)
184
+ return false unless Passlib.verify(password, user.password_hash)
185
+
186
+ if new_hash = Passlib.upgrade(password, user.password_hash)
187
+ user.update(password_hash: new_hash.to_s)
188
+ end
189
+
190
+ true
191
+ end
192
+ ```
193
+
194
+ Or a simple one-line upgrade:
195
+
196
+ ```ruby
197
+ hash = Passlib.upgrade(password, hash) || hash
198
+ ```
199
+
200
+ `upgrade` verifies the password before re-hashing by default. Pass `verify: false` to skip verification:
201
+
202
+ ```ruby
203
+ new_hash = Passlib.upgrade(password, stored_hash, verify: false)
204
+ ```
205
+
206
+ The `upgrade?` method on `Passlib` (or any other [context](#isolated-contexts)) returns `true` in two cases:
207
+
208
+ 1. The hash uses a different algorithm than the configured preferred scheme.
209
+ 2. It uses the right algorithm but with cost parameters that don't exactly match the current configuration.
210
+
211
+ For the second case, an exact match is required, so a hash with higher costs than configured is also considered outdated (allowing a downgrade when parameters were set too high for acceptable performance).
212
+
213
+ ### Check algorithm availability at runtime
214
+
215
+ Since some algorithms depend on optional gems, `Passlib.available?` lets you check at runtime whether a given algorithm can be used:
216
+
217
+ ```ruby
218
+ Passlib.available?(:argon2) # => true if the argon2 gem is installed, false otherwise
219
+ Passlib.available?(:bcrypt) # => true if the bcrypt gem is installed, false otherwise
220
+ Passlib.available?(:unknown) # => nil (unrecognized algorithm)
221
+ ```
222
+
223
+ ## Integration with other tools, libraries, and frameworks
224
+
225
+ ### Devise
226
+
227
+ Use the [devise-passlib](https://github.com/rkh/passlib/tree/main/devise-passlib) gem to integrate Passlib with Devise:
228
+
229
+ ```ruby
230
+ # app/models/user.rb
231
+ class User < ApplicationRecord
232
+ devise :database_authenticatable, :passlib
233
+ end
234
+ ```
235
+
236
+ It will respect the global Passlib configuration, but you can also specify options that only apply to devise hashes:
237
+
238
+ ```ruby
239
+ # config/initializers/devise.rb
240
+ Devise.setup do |config|
241
+ config.passlib.preferred_scheme = :argon2
242
+ config.passlib.argon2.profile = :rfc_9106_high_memory
243
+ end
244
+ ```
245
+
246
+ This is a drop-in replacement for Devise's default bcrypt implementation, so you can use it with any of the supported algorithms and change the configuration without needing to modify your models or database. With the above configuration, it will automatically upgrade existing bcrypt hashes to argon2 on login.
247
+
248
+ ## Configuration
249
+
250
+ ### Global configuration
251
+
252
+ `Passlib.config` (also aliased as `Passlib.configuration`) returns the global `Passlib::Configuration` object. Changes to it affect all calls made through the `Passlib` module.
253
+
254
+ ```ruby
255
+ Passlib.config.preferred_scheme = :argon2
256
+ Passlib.create("hunter2") # => #<Passlib::Argon2 "$argon2id$...">
257
+ ```
258
+
259
+ Use `Passlib.configure` (or its alias `Passlib.setup`) to apply several settings in one block:
260
+
261
+ ```ruby
262
+ Passlib.configure do |c|
263
+ c.preferred_scheme = :bcrypt
264
+ c.bcrypt.cost = 14
265
+ end
266
+ ```
267
+
268
+ ### Preferred scheme
269
+
270
+ `preferred_scheme=` sets the single algorithm used for new hashes. `preferred_schemes=` accepts an ordered list: Passlib picks the first one whose optional gem is available. This lets you specify a preference order without requiring every gem to be installed:
271
+
272
+ ```ruby
273
+ Passlib.config.preferred_schemes = [:argon2, :bcrypt, :sha2_crypt]
274
+ Passlib.create("hunter2") # => argon2 if available, otherwise bcrypt, etc.
275
+ ```
276
+
277
+ `preferred_scheme` (the reader, without `=`) returns the first available scheme from the list, or the configured single value.
278
+
279
+ #### Default setting
280
+
281
+ The default preference order is:
282
+
283
+ 1. [yescrypt](#yescrypt)
284
+ 2. [argon2](#argon2)
285
+ 3. [balloon](#balloon)
286
+ 4. [scrypt](#scrypt)
287
+ 5. [bcrypt](#bcrypt)
288
+ 6. [pbkdf2](#pbkdf2) with SHA3-512
289
+ 7. [pbkdf2](#pbkdf2) with SHA-512
290
+
291
+ The last option should be available in all environments since it only depends on OpenSSL, and works with older OpenSSL versions that don't support SHA3.
292
+
293
+ > [!NOTE]
294
+ > As you can see, [bcrypt-sha256](#bcrypt_sha256) is not included in the default, even though it improves security for long passwords. This is because it is not supported by any other Ruby library, but most environments come with bcrypt loaded by default, and adding it above bcrypt would cause all passwords to be re-hashed in a format that creates a hard dependency on Passlib.
295
+
296
+ ### Per-algorithm settings
297
+
298
+ Each algorithm has its own configuration namespace under `Passlib.config`. Algorithm-specific options (cost, rounds, variant, etc.) are set there:
299
+
300
+ ```ruby
301
+ Passlib.configure do |config|
302
+ config.bcrypt.cost = 14
303
+ config.argon2.t_cost = 4
304
+ config.argon2.m_cost = 18 # 2^18 = 256 MiB
305
+ config.sha2_crypt.rounds = 800_000
306
+ config.pbkdf2.rounds = 40_000
307
+ config.scrypt.ln = 17
308
+ end
309
+ ```
310
+
311
+ The full list of options for each algorithm is documented in the [Supported hash formats and algorithms](#supported-hash-formats-and-algorithms) section.
312
+
313
+ ### Configuration inheritance
314
+
315
+ `Passlib::Configuration` objects can inherit from a parent. A child reads the parent's value for any option it has not set itself, and changes to the parent propagate until the child is frozen.
316
+
317
+ ```ruby
318
+ base = Passlib::Configuration.new(preferred_scheme: :bcrypt)
319
+ base.bcrypt.cost = 12
320
+
321
+ child = Passlib::Configuration.new(base)
322
+ child.preferred_scheme # => :bcrypt (inherited)
323
+ child.bcrypt.cost # => 12 (inherited)
324
+
325
+ child.bcrypt.cost = 14
326
+ child.bcrypt.cost # => 14 (overridden)
327
+ base.bcrypt.cost # => 12 (unchanged)
328
+ ```
329
+
330
+ `freeze` snapshots all values from the parent chain into the child and prevents further mutation. The parent itself is not frozen:
331
+
332
+ ```ruby
333
+ child.freeze
334
+ base.bcrypt.cost = 16
335
+ child.bcrypt.cost # => 14 (snapshot is unaffected)
336
+ base.frozen? # => false
337
+ ```
338
+
339
+ ### Thread safety
340
+
341
+ The global configuration uses `Concurrent::Map` for its options store and is safe to read concurrently. Writes should be done at application startup before any concurrent access. For runtime isolation, create a `Passlib::Context` (see [isolated contexts](#isolated-contexts)) per request or tenant instead of mutating the global config.
342
+
343
+ ## Supported hash formats and algorithms
344
+
345
+ ### `argon2`
346
+
347
+ Uses the [argon2](https://github.com/technion/ruby-argon2/) gem, which is a wrapper around the reference C implementation of the Argon2 password hashing algorithm. This is the OWASP recommended password hashing algorithm, and is the winner of the Password Hashing Competition. It is designed to be resistant to GPU cracking attacks, and is highly configurable in terms of memory usage, time cost, and parallelism.
348
+
349
+ ``` ruby
350
+ hash = Passlib::Argon2.hash("password") # => #<Passlib::Argon2 "$argon2id$...">
351
+ hash.to_s # => "$argon2id$..."
352
+ hash.verify("password") # => true
353
+
354
+ # Pass options to argon2 gem:
355
+ Passlib::Argon2.hash("password", t_cost: 4, m_cost: 16) # => #<Passlib::Argon2 "$argon2id$...">
356
+
357
+ # Change the default options:
358
+ Passlib.config.argon2.profile = :rfc_9106_high_memory
359
+
360
+ # Set argon2 to be the default for new hashes:
361
+ Passlib.config.default_scheme = :argon2
362
+ Passlib.create("password") # => #<Passlib::Argon2 "$argon2id$...">
363
+ ```
364
+
365
+ Generated hashes are in the **Modular Crypt Format (MCF)**, using `argon2id` identifier.
366
+ It is also possible to load hashes with the IDs `argon2i` and `argon2d`.
367
+
368
+ Hash format:
369
+
370
+ ```ruby
371
+ "$argon2#d$v=#{v}$m=#{m},t=#{t},p=#{p}$#{salt}$#{digest}"
372
+ ```
373
+
374
+ The implementation does not (currently) support recreating the exact same hash.
375
+
376
+ ### `balloon`
377
+
378
+ Uses the [balloon_hashing](https://rubygems.org/gems/balloon_hashing) gem. Balloon hashing is a memory-hard password hashing function designed to be resistant to GPU and ASIC attacks.
379
+
380
+ ```ruby
381
+ hash = Passlib::Balloon.create("password") # => #<Passlib::Balloon "$balloon$...">
382
+ hash.verify("password") # => true
383
+
384
+ # With options:
385
+ Passlib::Balloon.create("password", s_cost: 1024, t_cost: 3, algorithm: "sha256")
386
+ ```
387
+
388
+ Generated hashes are in the **Modular Crypt Format (MCF)**.
389
+
390
+ Hash format:
391
+
392
+ ```ruby
393
+ "$balloon$v=1$alg=#{algorithm},s=#{s_cost},t=#{t_cost}$#{salt}$#{checksum}"
394
+ ```
395
+
396
+ Options:
397
+ - `:s_cost` — space cost (memory usage)
398
+ - `:t_cost` — time cost (iterations)
399
+ - `:algorithm` — digest algorithm name (e.g. `"sha256"`)
400
+
401
+ ### `bcrypt`
402
+
403
+ Standard hashing algorithm used by Rails and Devise. Uses the [bcrypt](https://github.com/bcrypt-ruby/bcrypt-ruby) gem. It uses the Blowfish cipher internally, and is designed to be slow and resistant to brute-force attacks. It is widely supported and has been around for a long time, but is no longer considered the best choice for password hashing due to its relatively low memory usage.
404
+
405
+ ```ruby
406
+ hash = Passlib::BCrypt.create("password", cost: 12)
407
+ hash.verify("password") # => true
408
+ hash.to_s # => "$2a$12$..."
409
+ ```
410
+
411
+ Generated hashes are in the **Modular Crypt Format (MCF)**, using the `$2a$` identifier. Hashes with the IDs `$2b$`, `$2x$`, and `$2y$` are also accepted on load.
412
+
413
+ Hash format:
414
+
415
+ ```ruby
416
+ "$2a$#{cost}$#{salt}#{checksum}"
417
+ ```
418
+
419
+ Recreating the exact same hash is supported via `:salt`.
420
+
421
+ Options:
422
+ - `:cost` — bcrypt cost factor, 4–31 (default: `BCrypt::Engine::DEFAULT_COST`)
423
+ - `:salt` — custom bcrypt salt string (normally auto-generated, must include the cost factor in standard bcrypt format)
424
+
425
+ ### `bcrypt_sha256`
426
+
427
+ A hybrid scheme that works around bcrypt's 72-byte password truncation limit. The password is first run through HMAC-SHA256 (keyed with the bcrypt salt), the 32-byte result is base64-encoded, and that string is then hashed with standard bcrypt. Passwords of any length are handled correctly. Uses the [bcrypt](https://github.com/bcrypt-ruby/bcrypt-ruby) gem.
428
+
429
+ This format is compatible with Python's [passlib.hash.bcrypt_sha256](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt_sha256.html).
430
+
431
+ ```ruby
432
+ hash = Passlib::BcryptSHA256.create("password", cost: 12)
433
+ hash.verify("password") # => true
434
+ hash.to_s # => "$bcrypt-sha256$v=2,t=2b,r=12$..."
435
+
436
+ # Long passwords are not truncated:
437
+ long = "a" * 100
438
+ Passlib::BcryptSHA256.create(long).verify(long) # => true
439
+ Passlib::BcryptSHA256.create(long).verify("a" * 99 + "b") # => false
440
+ ```
441
+
442
+ Generated hashes are in the **Modular Crypt Format (MCF)**, using the `$bcrypt-sha256$` identifier.
443
+
444
+ Hash format:
445
+
446
+ ```
447
+ $bcrypt-sha256$v=2,t=2b,r=#{cost}$#{salt22}$#{digest31}
448
+ ```
449
+
450
+ Options:
451
+ - `:cost` — bcrypt cost factor, 4–31 (default: `BCrypt::Engine::DEFAULT_COST`)
452
+ - `:salt` — custom bcrypt salt string in `$2b$NN$<22chars>` format (normally auto-generated)
453
+
454
+ ### `ldap_digest`
455
+
456
+ > [!WARNING]
457
+ > Not all of the supported variants are considered secure by modern standards. Moreover, even the ones that are aren't suitable for new hashes due to their low iteration count and lack of memory hardness (they aren't key derivation functions). They are supported here to verify existing hashes and migrate users to a stronger scheme on next login. Do not use them for new password hashes.
458
+
459
+ LDAP RFC 2307-style password hashes using digest algorithms. Implemented using OpenSSL, no additional gem dependencies.
460
+
461
+ Plain (unsalted) and salted variants are supported for MD5, SHA-1, SHA-256, SHA-512, SHA3-256, and SHA3-512. MD5 and SHA-1 hashes may be stored in hex encoding (as produced by some LDAP implementations) or standard base64, both are detected automatically on load.
462
+
463
+ ```ruby
464
+ hash = Passlib::LdapDigest.create("password", variant: "SSHA512")
465
+ hash.verify("password") # => true
466
+ hash.to_s # => "{SSHA512}..."
467
+ ```
468
+
469
+ Supported variants (LDAP scheme names):
470
+
471
+ | Variant | Algorithm | Salted |
472
+ |-------------|-----------|--------|
473
+ | `MD5` | MD5 | no |
474
+ | `SMD5` | MD5 | yes |
475
+ | `SHA` | SHA-1 | no |
476
+ | `SSHA` | SHA-1 | yes |
477
+ | `SHA256` | SHA-256 | no |
478
+ | `SSHA256` | SHA-256 | yes |
479
+ | `SHA512` | SHA-512 | no |
480
+ | `SSHA512` | SHA-512 | yes |
481
+ | `SHA3-256` | SHA3-256 | no |
482
+ | `SSHA3-256` | SHA3-256 | yes |
483
+ | `SHA3-512` | SHA3-512 | no |
484
+ | `SSHA3-512` | SHA3-512 | yes |
485
+
486
+ Hash format:
487
+
488
+ ```ruby
489
+ "{#{variant}}#{checksum_b64}" # base64 encoding (default)
490
+ "{#{variant}}#{checksum_hex}" # hex encoding (MD5 and SHA only)
491
+ ```
492
+
493
+ Default variant: `SSHA512`.
494
+
495
+ Options:
496
+ - `:variant` — LDAP scheme name (case-insensitive, symbols accepted, underscores may substitute dashes)
497
+ - `:salt` — raw binary salt (only used for salted schemes, default: 4 random bytes for MD5/SHA-1, 8 for others)
498
+ - `:hex` — encode as hex instead of base64, only applicable to `MD5` and `SHA` schemes (default: `false`)
499
+
500
+ ### `md5_crypt`
501
+
502
+ > [!WARNING]
503
+ > MD5-crypt is a legacy algorithm with a fixed, low round count that is vulnerable to brute-force attacks with modern hardware. It is supported here to verify existing hashes and migrate users to a stronger scheme. Do not use it for new hashes.
504
+
505
+ MD5-crypt was designed by Poul-Henning Kamp for FreeBSD in 1994 and was for many years the default password scheme on Linux systems. It runs 1000 rounds of an MD5-based mixing function. Implemented using OpenSSL, no additional gem dependencies.
506
+
507
+ Apache's variant (`$apr1$`) is also supported. The two variants are algorithmically identical and differ only in their MCF identifier. Both are auto-detected on load.
508
+
509
+ ```ruby
510
+ hash = Passlib::MD5Crypt.create("hunter2")
511
+ hash.verify("hunter2") # => true
512
+ hash.to_s # => "$1$...$..."
513
+
514
+ apr = Passlib::MD5Crypt.create("hunter2", variant: :apr)
515
+ apr.to_s # => "$apr1$...$..."
516
+ ```
517
+
518
+ Hash format:
519
+
520
+ ```ruby
521
+ "$1$#{salt}$#{checksum}" # standard MD5-crypt
522
+ "$apr1$#{salt}$#{checksum}" # Apache APR variant
523
+ ```
524
+
525
+ - `salt` — 0-8 characters from `./0-9A-Za-z` (default: 8 random characters)
526
+ - `checksum` — 22-character encoding of the 16-byte MD5 digest
527
+
528
+ Options:
529
+ - `:variant` — selects the MCF identifier: `:standard` (default, produces `$1$`) or `:apr` (produces `$apr1$`)
530
+ - `:salt` — custom salt string, 0-8 characters (default: 8 random characters)
531
+
532
+ ### `pbkdf2`
533
+
534
+ PBKDF2 hashes via OpenSSL, no additional gem dependencies. New hashes are always produced in the **Modular Crypt Format (MCF)**. Two additional formats are accepted on load and normalized to MCF: LDAP-style hashes and Cryptacular's `cta_pbkdf2_sha1` format.
535
+
536
+ ```ruby
537
+ hash = Passlib::PBKDF2.create("password", variant: "pbkdf2-sha256", rounds: 29_000)
538
+ hash.verify("password") # => true
539
+ hash.to_s # => "$pbkdf2-sha256$29000$...$..."
540
+
541
+ # Load and normalize an LDAP hash to MCF:
542
+ Passlib::PBKDF2.load("{PBKDF2-SHA256}29000$...$...").to_s
543
+ # => "$pbkdf2-sha256$29000$...$..."
544
+
545
+ # Load and normalize a Cryptacular cta_pbkdf2_sha1 hash to MCF:
546
+ Passlib::PBKDF2.load("$p5k2$2710$...$...").to_s
547
+ # => "$pbkdf2$10000$...$..."
548
+ ```
549
+
550
+ Hash format (MCF, used for new hashes):
551
+
552
+ ```ruby
553
+ "$#{variant}$#{rounds}$#{salt_ab64}$#{dk_ab64}"
554
+ ```
555
+
556
+ Cryptacular `cta_pbkdf2_sha1` format (accepted on load, normalized to MCF):
557
+
558
+ ```ruby
559
+ "$p5k2$#{rounds_hex}$#{salt_b64url}$#{dk_b64url}"
560
+ ```
561
+
562
+ Supported variants:
563
+
564
+ | MCF variant | LDAP variant | Digest | Default rounds | Key length |
565
+ |--------------------|--------------------|----------|----------------|------------|
566
+ | `pbkdf2` | `PBKDF2` | SHA-1 | 131,000 | 20 bytes |
567
+ | `pbkdf2-sha256` | `PBKDF2-SHA256` | SHA-256 | 29,000 | 32 bytes |
568
+ | `pbkdf2-sha512` | `PBKDF2-SHA512` | SHA-512 | 25,000 | 64 bytes |
569
+ | `pbkdf2-sha3-256` | `PBKDF2-SHA3-256` | SHA3-256 | 29,000 | 32 bytes |
570
+ | `pbkdf2-sha3-512` | `PBKDF2-SHA3-512` | SHA3-512 | 25,000 | 64 bytes |
571
+
572
+ Default variant: `pbkdf2-sha3-512` if the OpenSSL build supports SHA3, otherwise `pbkdf2-sha512`.
573
+
574
+ Options:
575
+ - `:variant` — digest variant (case-insensitive, symbols accepted, underscores may substitute dashes)
576
+ - `:rounds` — iteration count (default: variant-specific, see table above)
577
+ - `:salt` — raw binary salt (default: 16 random bytes)
578
+ - `:key_len` — derived key length in bytes (default: variant-specific, see table above)
579
+
580
+ ### `phpass`
581
+
582
+ > [!WARNING]
583
+ > `phpass` is a legacy algorithm based on MD5 and should not be used for new hashes. It is supported here to verify existing hashes and migrate users to a stronger scheme on next login.
584
+
585
+ The phpass Portable Hash, widely used by PHP applications such as WordPress, Drupal, and phpBB as a fallback password scheme. It applies iterated MD5 with a configurable round count. Implemented using OpenSSL, no additional gem dependencies.
586
+
587
+ Both the standard `$P$` identifier and the phpBB3 `$H$` variant are recognized on load. New hashes are always produced with `$P$`.
588
+
589
+ ```ruby
590
+ hash = Passlib::PHPass.create("password", rounds: 19)
591
+ hash.verify("password") # => true
592
+ hash.to_s # => "$P$H..."
593
+
594
+ # Load and verify a $H$ (phpBB3) hash without any conversion:
595
+ Passlib::PHPass.load("$H$9IQRg...").verify("password")
596
+ ```
597
+
598
+ Hash format:
599
+
600
+ ```ruby
601
+ "$P$#{rounds}#{salt}#{checksum}"
602
+ ```
603
+
604
+ - `rounds` — single character from the phpass alphabet encoding `log2(iterations)`, 7–30
605
+ - `salt` — 8-character salt using the phpass alphabet `./0-9A-Za-z`
606
+ - `checksum` — 22-character encoding of the 16-byte MD5 digest
607
+
608
+ Options:
609
+ - `:rounds` — base-2 log of the iteration count, 7–30, clamped if out of range (default: 19, i.e. 2^19 = 524 288 iterations)
610
+ - `:salt` — custom 8-character salt using the phpass alphabet (normally auto-generated)
611
+
612
+ ### `scrypt`
613
+
614
+ Uses the [scrypt](https://rubygems.org/gems/scrypt) gem. scrypt is a memory-hard key derivation function designed to be expensive in both CPU and memory, making it resistant to brute-force attacks with custom hardware.
615
+
616
+ ```ruby
617
+ hash = Passlib::SCrypt.create("password", ln: 14)
618
+ hash.verify("password") # => true
619
+ hash.to_s # => "$scrypt$ln=14,r=8,p=1$...$..."
620
+ ```
621
+
622
+ Generated hashes are in the **Modular Crypt Format (MCF)**. Two hash formats are accepted on load:
623
+
624
+ ```ruby
625
+ "$scrypt$ln=#{ln},r=#{r},p=#{p}$#{salt}$#{checksum}" # Passlib MCF
626
+ "#{n_hex}$#{r_hex}$#{p_hex}$#{salt_hex}$#{checksum_hex}" # scrypt gem native (normalized to MCF on load)
627
+ ```
628
+
629
+ New hashes are always produced in the Passlib MCF format.
630
+
631
+ Options:
632
+ - `:ln` — CPU/memory cost as a base-2 log (default: `16`, meaning N=65536), mutually exclusive with `:n`
633
+ - `:n` — CPU/memory cost as an integer power of two (converted to `:ln` internally)
634
+ - `:r` — block size (default: `8`)
635
+ - `:p` — parallelization factor (default: `1`)
636
+ - `:salt` — custom salt as a binary string (default: 16 random bytes)
637
+ - `:key_len` — derived key length in bytes (default: `32`)
638
+
639
+ ### `sha1_crypt`
640
+
641
+ > [!WARNING]
642
+ > `sha1_crypt` is a legacy algorithm and should not be used for new hashes. It is supported here to verify existing hashes and migrate users to a stronger scheme on next login.
643
+
644
+ NetBSD's crypt-sha1 algorithm designed by Simon Gerraty. It applies iterated HMAC-SHA1 and supports passwords of any length. Implemented using OpenSSL, no additional gem dependencies.
645
+
646
+ ```ruby
647
+ hash = Passlib::SHA1Crypt.create("password", rounds: 480_000)
648
+ hash.verify("password") # => true
649
+ hash.to_s # => "$sha1$480000$...$..."
650
+ ```
651
+
652
+ Hash format:
653
+
654
+ ```ruby
655
+ "$sha1$#{rounds}$#{salt}$#{checksum}"
656
+ ```
657
+
658
+ - `rounds` — decimal iteration count, 1–4,294,967,295
659
+ - `salt` — 1–64 characters from `./0-9A-Za-z` (default: 8 random characters)
660
+ - `checksum` — 28-character encoding of the 20-byte HMAC-SHA1 digest
661
+
662
+ Options:
663
+ - `:rounds` — iteration count, 1–4,294,967,295, clamped if out of range (default: 480,000)
664
+ - `:salt` — custom salt string, up to 64 characters (default: 8 random characters)
665
+
666
+ ### `sha2_crypt`
667
+
668
+ Pure-Ruby implementation of the SHA-crypt algorithm as specified by Ulrich Drepper. Implemented using OpenSSL, no additional gem dependencies.
669
+
670
+ Generated hashes are in the **Modular Crypt Format (MCF)**. Both SHA-256 (`$5$`) and SHA-512 (`$6$`) variants are supported and auto-detected on load.
671
+
672
+ ```ruby
673
+ hash = Passlib::SHA2Crypt.create("password", bits: 512, rounds: 10_000)
674
+ hash.verify("password") # => true
675
+ hash.to_s # => "$6$rounds=10000$...$..."
676
+ ```
677
+
678
+ Hash format:
679
+
680
+ ```ruby
681
+ "$#{id}$rounds=#{rounds}$#{salt}$#{checksum}" # explicit rounds
682
+ "$#{id}$#{salt}$#{checksum}" # implicit rounds (5,000)
683
+ ```
684
+
685
+ | Variant | `id` | Digest | Default rounds | Checksum length |
686
+ |---------|------|---------|----------------|-----------------|
687
+ | SHA-256 | `5` | SHA-256 | 535,000 | 43 characters |
688
+ | SHA-512 | `6` | SHA-512 | 656,000 | 86 characters |
689
+
690
+ Rounds are clamped to the range 1,000–999,999,999. If no `rounds=` parameter appears in the hash string, 5,000 rounds are assumed (per spec).
691
+
692
+ Options:
693
+ - `:bits` — selects the SHA variant: `256` or `512` (default: `512`)
694
+ - `:rounds` — number of hashing rounds (default: variant-specific, see table above)
695
+ - `:salt` — custom salt string, up to 16 characters (default: random)
696
+
697
+ ### `yescrypt`
698
+
699
+ Uses the [yescrypt](https://rubygems.org/gems/yescrypt) gem. yescrypt is the successor to scrypt, used as the default password hashing scheme in several modern Linux distributions.
700
+
701
+ ```ruby
702
+ hash = Passlib::Yescrypt.create("password")
703
+ hash.verify("password") # => true
704
+ hash.to_s # => "$y$..."
705
+ ```
706
+
707
+ Generated hashes are in the **Modular Crypt Format (MCF)**.
708
+
709
+ Hash format:
710
+
711
+ ```ruby
712
+ "$y$#{params}$#{salt}$#{checksum}"
713
+ ```
714
+
715
+ Options:
716
+ - `:n_log2` — base-2 log of the memory/CPU cost factor
717
+ - `:r` — block size
718
+ - `:p` — parallelization factor
719
+ - `:t` — additional time parameter
720
+ - `:flags` — algorithm flags bitmask
721
+ - `:salt` — custom salt (normally auto-generated)
722
+
723
+
724
+ ## License
725
+
726
+ Passlib is released under the MIT License. See [MIT-LICENSE](MIT-LICENSE) for details.