PBKDF2 and bcrypt are password hashing functions, but they don’t just hash passwords; they deliberately slow down the process to make brute-forcing computationally infeasible.
Let’s see what happens when we try to hash a password with openssl and bcrypt and then verify it.
Hashing with OpenSSL (PBKDF2)
First, we need a salt. A salt is random data added to the password before hashing. This ensures that even if two users have the same password, their hashes will be different.
# Generate a random salt (e.g., 16 bytes)
openssl rand -hex 16
# Output: a1b2c3d4e5f60718293a4b5c6d7e8f90
Now, let’s use this salt with PBKDF2. We’ll specify the desired hash algorithm (e.g., SHA256) and the number of iterations. A higher iteration count means more computation, making it slower for attackers.
# Hash the password "mysecretpassword" with PBKDF2-SHA256
# Iterations: 310000 (a common recommended value)
echo -n "mysecretpassword" | openssl dgst -sha256 -pbkdf2 -iter 310000 -salt -hex -pass file:/dev/stdin -s "a1b2c3d4e5f60718293a4b5c6d7e8f90"
# Output: a1b2c3d4e5f60718293a4b5c6d7e8f90$310000$256$a1b2c3d4e5f60718293a4b5c6d7e8f90$1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
The output format is generally salt$iterations$digest_length$salt_bytes$hash_bytes. The important part is that it includes the salt and the iteration count, which are needed for verification.
Verifying with OpenSSL (PBKDF2)
To verify, we need the original password, the salt, and the iteration count.
# Verify the password "mysecretpassword" against the stored hash
echo -n "mysecretpassword" | openssl dgst -sha256 -pbkdf2 -iter 310000 -salt -hex -pass file:/dev/stdin -s "a1b2c3d4e5f60718293a4b5c6d7e8f90"
# Output: a1b2c3d4e5f60718293a4b5c6d7e8f90$310000$256$a1b2c3d4e5f60718293a4b5c6d7e8f90$1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
If the output matches the stored hash, the password is correct.
Hashing with bcrypt
bcrypt uses a different approach. It’s designed to be computationally expensive by incorporating a "work factor" (cost parameter) and a salt directly into the resulting hash string.
# Install bcrypt if you don't have it
# sudo apt-get install bcrypt (Debian/Ubuntu)
# brew install bcrypt (macOS)
# Hash the password "mysecretpassword" with bcrypt
# Cost factor: 10 (a common starting point)
echo -n "mysecretpassword" | bcrypt -n 10
# Output: $2a$10$abcdefghijklmnopqrstuv.xyza1b2c3d4e5f60718293a4b5c6d7e8f90
The bcrypt output format is $2a$cost$salt_and_hash. The salt_and_hash part contains both the salt and the actual hash.
Verifying with bcrypt
Verification with bcrypt is even simpler because the salt and cost are embedded in the hash string.
# Verify the password "mysecretpassword" against the stored bcrypt hash
echo -n "mysecretpassword" | bcrypt -v '$2a$10$abcdefghijklmnopqrstuv.xyza1b2c3d4e5f60718293a4b5c6d7e8f90'
# Output: password matches
If the password matches, bcrypt -v will output "password matches". If it doesn’t, it will exit with a non-zero status code and potentially print an error.
The core problem these functions solve is the speed of brute-force attacks. A naive hash like SHA-256 on a password would be hashed millions of times per second on modern hardware. PBKDF2 and bcrypt are intentionally slow.
PBKDF2 (Password-Based Key Derivation Function 2) works by repeatedly applying a pseudorandom function (like HMAC-SHA256) to the password, a salt, and a counter, for a specified number of iterations. The higher the iteration count, the more CPU time is required to compute the hash. This makes it prohibitively expensive for an attacker to try millions of passwords per second. The standard specifies a minimum of 100,000 iterations, and many recommend 310,000 or more.
bcrypt, on the other hand, is a key-derivation function based on the Blowfish cipher. It uses a computationally intensive algorithm that includes a "cost factor" (or work factor). This cost factor is an exponent that determines how many times the underlying algorithm runs. A cost factor of 10 means the algorithm runs 2^10 times. Increasing this cost factor exponentially increases the time it takes to compute a hash, effectively slowing down brute-force attempts. Modern recommendations often start at a cost factor of 10 or 12, and this should be tuned based on server hardware capabilities.
The key difference in how they are used is that PBKDF2 requires you to store the salt and the iteration count separately from the hash, whereas bcrypt embeds both the salt and the cost factor directly into the output hash string. This makes bcrypt’s output self-contained and often simpler to manage.
A common misconception is that a very high iteration count or cost factor is always better. While it’s true that more computation is better for security, you must balance this against the performance impact on your login system. If your server takes too long to hash and verify passwords during login, it can lead to a poor user experience or even denial-of-service. The optimal values for iteration counts (PBKDF2) and cost factors (bcrypt) are dynamic and should be periodically re-evaluated and increased as hardware capabilities improve and your system’s performance allows. You can use tools like pwntest or simply benchmark your login times to find suitable values.
The next problem you’ll encounter is efficiently managing these salted and iterated hashes within your application’s database and user authentication logic.