Passlib

A Ruby password hashing library inspired by Python's Passlib. 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.

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.

Table of Contents

Installation

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.

gem install passlib

# Optional algorithm dependencies (install any combination):
gem install bcrypt
gem install argon2
gem install scrypt
gem install balloon_hashing
gem install yescrypt

To install passlib together with all optional dependencies at once, use passlib-all:

gem install passlib-all

With Bundler, add only what you need:

# Gemfile
gem "passlib"

# Optional, add any combination:
gem "bcrypt"
gem "argon2"
gem "scrypt"
gem "balloon_hashing"
gem "yescrypt"

Or pull in everything at once:

# Gemfile
gem "passlib-all"

Features

Create and verify hashes

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.

# Hash a new password (uses the configured default algorithm)
hash = Passlib.create("hunter2")
hash.to_s              # => "$argon2id$..."
hash.verify("hunter2") # => true
hash.verify("wrong")   # => false

# Load a stored hash and verify
hash = Passlib.load("$argon2id$...")
hash.verify("hunter2") # => true

Passlib.load auto-detects the algorithm from the hash string, so you can verify hashes without knowing which algorithm produced them:

Passlib.load("$2a$12$...").verify("hunter2")    # bcrypt
Passlib.load("$argon2id$...").verify("hunter2") # argon2
Passlib.load("{SSHA512}...").verify("hunter2")  # LDAP

For the common case of verifying a stored hash, Passlib.verify combines load and match in one call:

Passlib.verify("hunter2", "$argon2id$...") # => true

The verify methods are also aliased as matches?, valid_password?, valid_secret?, and on Password instances as ===, which allows usage in case statements:

hash = Passlib.load("$argon2id$...")

case "hunter2"
when hash then puts "Password is correct"
else puts "Password is incorrect"
end

Recreating hashes

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:

if hash.respond_to?(:create_comparable)
  rehashed = hash.create_comparable("hunter2")
  Passlib.secure_compare(rehashed, hash) # => true if passwords match
end

Isolated contexts

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.

context = Passlib::Context.new(preferred_scheme: :bcrypt)
hash    = context.create("hunter2")
hash.class                      # => Passlib::BCrypt
context.verify("hunter2", hash) # => true

A context can inherit from another context or configuration object, so you can share a base setup and override only what you need:

base    = Passlib::Context.new(preferred_scheme: :bcrypt)
context = Passlib::Context.new(base)
context.create("hunter2").class # => Passlib::BCrypt

You can also reconfigure a context after creation using configure:

context = Passlib::Context.new
context.configure { |c| c.preferred_scheme = :bcrypt }

A context exposes the same interface as the Passlib module.

Upgrading password hashes

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.

Passlib.config.preferred_scheme = :bcrypt
Passlib.config.bcrypt.cost = 12

# Check whether a hash is outdated
Passlib.upgrade?("$2a$04$...")   # => true  (cost 4 is below the configured 12)
Passlib.upgrade?("$2a$12$...")   # => false (already at cost 12)
Passlib.upgrade?("{SSHA512}...") # => true  (wrong algorithm)

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:

def (user, password)
  return false unless Passlib.verify(password, user.password_hash)

  if new_hash = Passlib.upgrade(password, user.password_hash)
    user.update(password_hash: new_hash.to_s)
  end

  true
end

Or a simple one-line upgrade:

hash = Passlib.upgrade(password, hash) || hash

upgrade verifies the password before re-hashing by default. Pass verify: false to skip verification:

new_hash = Passlib.upgrade(password, stored_hash, verify: false)

The upgrade? method on Passlib (or any other context) returns true in two cases:

  1. The hash uses a different algorithm than the configured preferred scheme.
  2. It uses the right algorithm but with cost parameters that don't exactly match the current configuration.

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).

Check algorithm availability at runtime

Since some algorithms depend on optional gems, Passlib.available? lets you check at runtime whether a given algorithm can be used:

Passlib.available?(:argon2)  # => true if the argon2 gem is installed, false otherwise
Passlib.available?(:bcrypt)  # => true if the bcrypt gem is installed, false otherwise
Passlib.available?(:unknown) # => nil (unrecognized algorithm)

Integration with other tools, libraries, and frameworks

Devise

Use the devise-passlib gem to integrate Passlib with Devise:

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :passlib
end

It will respect the global Passlib configuration, but you can also specify options that only apply to devise hashes:

# config/initializers/devise.rb
Devise.setup do |config|
  config.passlib.preferred_scheme = :argon2
  config.passlib.argon2.profile = :rfc_9106_high_memory
end

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.

Configuration

Global configuration

Passlib.config (also aliased as Passlib.configuration) returns the global Passlib::Configuration object. Changes to it affect all calls made through the Passlib module.

Passlib.config.preferred_scheme = :argon2
Passlib.create("hunter2") # => #<Passlib::Argon2 "$argon2id$...">

Use Passlib.configure (or its alias Passlib.setup) to apply several settings in one block:

Passlib.configure do |c|
  c.preferred_scheme = :bcrypt
  c.bcrypt.cost      = 14
end

Preferred scheme

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:

Passlib.config.preferred_schemes = [:argon2, :bcrypt, :sha2_crypt]
Passlib.create("hunter2") # => argon2 if available, otherwise bcrypt, etc.

preferred_scheme (the reader, without =) returns the first available scheme from the list, or the configured single value.

Default setting

The default preference order is:

  1. yescrypt
  2. argon2
  3. balloon
  4. scrypt
  5. bcrypt
  6. pbkdf2 with SHA3-512
  7. pbkdf2 with SHA-512

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.

[!NOTE] As you can see, 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.

Per-algorithm settings

Each algorithm has its own configuration namespace under Passlib.config. Algorithm-specific options (cost, rounds, variant, etc.) are set there:

Passlib.configure do |config|
  config.bcrypt.cost      = 14
  config.argon2.t_cost    = 4
  config.argon2.m_cost    = 18  # 2^18 = 256 MiB
  config.sha2_crypt.rounds = 800_000
  config.pbkdf2.rounds    = 40_000
  config.scrypt.ln        = 17
end

The full list of options for each algorithm is documented in the Supported hash formats and algorithms section.

Configuration inheritance

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.

base = Passlib::Configuration.new(preferred_scheme: :bcrypt)
base.bcrypt.cost = 12

child = Passlib::Configuration.new(base)
child.preferred_scheme # => :bcrypt (inherited)
child.bcrypt.cost      # => 12      (inherited)

child.bcrypt.cost = 14
child.bcrypt.cost # => 14 (overridden)
base.bcrypt.cost  # => 12 (unchanged)

freeze snapshots all values from the parent chain into the child and prevents further mutation. The parent itself is not frozen:

child.freeze
base.bcrypt.cost = 16
child.bcrypt.cost # => 14 (snapshot is unaffected)
base.frozen?      # => false

Thread safety

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) per request or tenant instead of mutating the global config.

Supported hash formats and algorithms

argon2

Uses the 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.

hash = Passlib::Argon2.hash("password") # => #<Passlib::Argon2 "$argon2id$...">
hash.to_s                               # => "$argon2id$..."
hash.verify("password")                 # => true

# Pass options to argon2 gem:
Passlib::Argon2.hash("password", t_cost: 4, m_cost: 16) # => #<Passlib::Argon2 "$argon2id$...">

# Change the default options:
Passlib.config.argon2.profile = :rfc_9106_high_memory

# Set argon2 to be the default for new hashes:
Passlib.config.default_scheme = :argon2
Passlib.create("password") # => #<Passlib::Argon2 "$argon2id$...">

Generated hashes are in the Modular Crypt Format (MCF), using argon2id identifier. It is also possible to load hashes with the IDs argon2i and argon2d.

Hash format:

"$argon2#d$v=#{v}$m=#{m},t=#{t},p=#{p}$#{salt}$#{digest}"

The implementation does not (currently) support recreating the exact same hash.

balloon

Uses the balloon_hashing gem. Balloon hashing is a memory-hard password hashing function designed to be resistant to GPU and ASIC attacks.

hash = Passlib::Balloon.create("password") # => #<Passlib::Balloon "$balloon$...">
hash.verify("password") # => true

# With options:
Passlib::Balloon.create("password", s_cost: 1024, t_cost: 3, algorithm: "sha256")

Generated hashes are in the Modular Crypt Format (MCF).

Hash format:

"$balloon$v=1$alg=#{algorithm},s=#{s_cost},t=#{t_cost}$#{salt}$#{checksum}"

Options:

  • :s_cost — space cost (memory usage)
  • :t_cost — time cost (iterations)
  • :algorithm — digest algorithm name (e.g. "sha256")

bcrypt

Standard hashing algorithm used by Rails and Devise. Uses the bcrypt 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.

hash = Passlib::BCrypt.create("password", cost: 12)
hash.verify("password") # => true
hash.to_s               # => "$2a$12$..."

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.

Hash format:

"$2a$#{cost}$#{salt}#{checksum}"

Recreating the exact same hash is supported via :salt.

Options:

  • :cost — bcrypt cost factor, 4–31 (default: BCrypt::Engine::DEFAULT_COST)
  • :salt — custom bcrypt salt string (normally auto-generated, must include the cost factor in standard bcrypt format)

bcrypt_sha256

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 gem.

This format is compatible with Python's passlib.hash.bcrypt_sha256.

hash = Passlib::BcryptSHA256.create("password", cost: 12)
hash.verify("password") # => true
hash.to_s               # => "$bcrypt-sha256$v=2,t=2b,r=12$..."

# Long passwords are not truncated:
long = "a" * 100
Passlib::BcryptSHA256.create(long).verify(long)          # => true
Passlib::BcryptSHA256.create(long).verify("a" * 99 + "b") # => false

Generated hashes are in the Modular Crypt Format (MCF), using the $bcrypt-sha256$ identifier.

Hash format:

$bcrypt-sha256$v=2,t=2b,r=#{cost}$#{salt22}$#{digest31}

Options:

  • :cost — bcrypt cost factor, 4–31 (default: BCrypt::Engine::DEFAULT_COST)
  • :salt — custom bcrypt salt string in $2b$NN$<22chars> format (normally auto-generated)

ldap_digest

[!WARNING] 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.

LDAP RFC 2307-style password hashes using digest algorithms. Implemented using OpenSSL, no additional gem dependencies.

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.

hash = Passlib::LdapDigest.create("password", variant: "SSHA512")
hash.verify("password") # => true
hash.to_s               # => "{SSHA512}..."

Supported variants (LDAP scheme names):

Variant Algorithm Salted
MD5 MD5 no
SMD5 MD5 yes
SHA SHA-1 no
SSHA SHA-1 yes
SHA256 SHA-256 no
SSHA256 SHA-256 yes
SHA512 SHA-512 no
SSHA512 SHA-512 yes
SHA3-256 SHA3-256 no
SSHA3-256 SHA3-256 yes
SHA3-512 SHA3-512 no
SSHA3-512 SHA3-512 yes

Hash format:

"{#{variant}}#{checksum_b64}" # base64 encoding (default)
"{#{variant}}#{checksum_hex}" # hex encoding (MD5 and SHA only)

Default variant: SSHA512.

Options:

  • :variant — LDAP scheme name (case-insensitive, symbols accepted, underscores may substitute dashes)
  • :salt — raw binary salt (only used for salted schemes, default: 4 random bytes for MD5/SHA-1, 8 for others)
  • :hex — encode as hex instead of base64, only applicable to MD5 and SHA schemes (default: false)

md5_crypt

[!WARNING] 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.

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.

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.

hash = Passlib::MD5Crypt.create("hunter2")
hash.verify("hunter2") # => true
hash.to_s              # => "$1$...$..."

apr = Passlib::MD5Crypt.create("hunter2", variant: :apr)
apr.to_s # => "$apr1$...$..."

Hash format:

"$1$#{salt}$#{checksum}"    # standard MD5-crypt
"$apr1$#{salt}$#{checksum}" # Apache APR variant
  • salt — 0-8 characters from ./0-9A-Za-z (default: 8 random characters)
  • checksum — 22-character encoding of the 16-byte MD5 digest

Options:

  • :variant — selects the MCF identifier: :standard (default, produces $1$) or :apr (produces $apr1$)
  • :salt — custom salt string, 0-8 characters (default: 8 random characters)

pbkdf2

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.

hash = Passlib::PBKDF2.create("password", variant: "pbkdf2-sha256", rounds: 29_000)
hash.verify("password") # => true
hash.to_s               # => "$pbkdf2-sha256$29000$...$..."

# Load and normalize an LDAP hash to MCF:
Passlib::PBKDF2.load("{PBKDF2-SHA256}29000$...$...").to_s
# => "$pbkdf2-sha256$29000$...$..."

# Load and normalize a Cryptacular cta_pbkdf2_sha1 hash to MCF:
Passlib::PBKDF2.load("$p5k2$2710$...$...").to_s
# => "$pbkdf2$10000$...$..."

Hash format (MCF, used for new hashes):

"$#{variant}$#{rounds}$#{salt_ab64}$#{dk_ab64}"

Cryptacular cta_pbkdf2_sha1 format (accepted on load, normalized to MCF):

"$p5k2$#{rounds_hex}$#{salt_b64url}$#{dk_b64url}"

Supported variants:

MCF variant LDAP variant Digest Default rounds Key length
pbkdf2 PBKDF2 SHA-1 131,000 20 bytes
pbkdf2-sha256 PBKDF2-SHA256 SHA-256 29,000 32 bytes
pbkdf2-sha512 PBKDF2-SHA512 SHA-512 25,000 64 bytes
pbkdf2-sha3-256 PBKDF2-SHA3-256 SHA3-256 29,000 32 bytes
pbkdf2-sha3-512 PBKDF2-SHA3-512 SHA3-512 25,000 64 bytes

Default variant: pbkdf2-sha3-512 if the OpenSSL build supports SHA3, otherwise pbkdf2-sha512.

Options:

  • :variant — digest variant (case-insensitive, symbols accepted, underscores may substitute dashes)
  • :rounds — iteration count (default: variant-specific, see table above)
  • :salt — raw binary salt (default: 16 random bytes)
  • :key_len — derived key length in bytes (default: variant-specific, see table above)

phpass

[!WARNING] 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.

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.

Both the standard $P$ identifier and the phpBB3 $H$ variant are recognized on load. New hashes are always produced with $P$.

hash = Passlib::PHPass.create("password", rounds: 19)
hash.verify("password") # => true
hash.to_s               # => "$P$H..."

# Load and verify a $H$ (phpBB3) hash without any conversion:
Passlib::PHPass.load("$H$9IQRg...").verify("password")

Hash format:

"$P$#{rounds}#{salt}#{checksum}"
  • rounds — single character from the phpass alphabet encoding log2(iterations), 7–30
  • salt — 8-character salt using the phpass alphabet ./0-9A-Za-z
  • checksum — 22-character encoding of the 16-byte MD5 digest

Options:

  • :rounds — base-2 log of the iteration count, 7–30, clamped if out of range (default: 19, i.e. 2^19 = 524 288 iterations)
  • :salt — custom 8-character salt using the phpass alphabet (normally auto-generated)

scrypt

Uses the 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.

hash = Passlib::SCrypt.create("password", ln: 14)
hash.verify("password") # => true
hash.to_s               # => "$scrypt$ln=14,r=8,p=1$...$..."

Generated hashes are in the Modular Crypt Format (MCF). Two hash formats are accepted on load:

"$scrypt$ln=#{ln},r=#{r},p=#{p}$#{salt}$#{checksum}" # Passlib MCF
"#{n_hex}$#{r_hex}$#{p_hex}$#{salt_hex}$#{checksum_hex}" # scrypt gem native (normalized to MCF on load)

New hashes are always produced in the Passlib MCF format.

Options:

  • :ln — CPU/memory cost as a base-2 log (default: 16, meaning N=65536), mutually exclusive with :n
  • :n — CPU/memory cost as an integer power of two (converted to :ln internally)
  • :r — block size (default: 8)
  • :p — parallelization factor (default: 1)
  • :salt — custom salt as a binary string (default: 16 random bytes)
  • :key_len — derived key length in bytes (default: 32)

sha1_crypt

[!WARNING] 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.

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.

hash = Passlib::SHA1Crypt.create("password", rounds: 480_000)
hash.verify("password") # => true
hash.to_s               # => "$sha1$480000$...$..."

Hash format:

"$sha1$#{rounds}$#{salt}$#{checksum}"
  • rounds — decimal iteration count, 1–4,294,967,295
  • salt — 1–64 characters from ./0-9A-Za-z (default: 8 random characters)
  • checksum — 28-character encoding of the 20-byte HMAC-SHA1 digest

Options:

  • :rounds — iteration count, 1–4,294,967,295, clamped if out of range (default: 480,000)
  • :salt — custom salt string, up to 64 characters (default: 8 random characters)

sha2_crypt

Pure-Ruby implementation of the SHA-crypt algorithm as specified by Ulrich Drepper. Implemented using OpenSSL, no additional gem dependencies.

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.

hash = Passlib::SHA2Crypt.create("password", bits: 512, rounds: 10_000)
hash.verify("password") # => true
hash.to_s               # => "$6$rounds=10000$...$..."

Hash format:

"$#{id}$rounds=#{rounds}$#{salt}$#{checksum}" # explicit rounds
"$#{id}$#{salt}$#{checksum}" # implicit rounds (5,000)
Variant id Digest Default rounds Checksum length
SHA-256 5 SHA-256 535,000 43 characters
SHA-512 6 SHA-512 656,000 86 characters

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).

Options:

  • :bits — selects the SHA variant: 256 or 512 (default: 512)
  • :rounds — number of hashing rounds (default: variant-specific, see table above)
  • :salt — custom salt string, up to 16 characters (default: random)

yescrypt

Uses the yescrypt gem. yescrypt is the successor to scrypt, used as the default password hashing scheme in several modern Linux distributions.

hash = Passlib::Yescrypt.create("password")
hash.verify("password") # => true
hash.to_s               # => "$y$..."

Generated hashes are in the Modular Crypt Format (MCF).

Hash format:

"$y$#{params}$#{salt}$#{checksum}"

Options:

  • :n_log2 — base-2 log of the memory/CPU cost factor
  • :r — block size
  • :p — parallelization factor
  • :t — additional time parameter
  • :flags — algorithm flags bitmask
  • :salt — custom salt (normally auto-generated)

License

Passlib is released under the MIT License. See MIT-LICENSE for details.