The Shack Security BCrypt Hashing Explained

BCrypt Hashing Explained: Why It's the Gold Standard for Passwords

Back to All Posts

Every data breach that exposes "hashed" passwords should be a reminder that not all hashes are created equal. Storing passwords with MD5 or SHA-256 is essentially storing them in plain text — the only thing standing between a breach and a full account takeover is the speed at which an attacker can run hashes. BCrypt's entire design philosophy is to make that speed a non-issue.

Try it yourself with the DevToolShack BCrypt Hash Generator — generate and verify bcrypt hashes instantly, free in your browser.

Why Fast Hashing Algorithms Are Wrong for Passwords

SHA-256 was designed to be fast. Modern GPUs can compute billions of SHA-256 hashes per second. This is great for cryptographic applications like signing certificates or verifying file integrity — but catastrophic for password storage.

Here's what that means in practice: if your database is breached and passwords are stored as SHA-256 hashes, an attacker with consumer GPU hardware can crack common passwords in seconds and most passwords within days. The entire password database becomes readable shortly after the breach.

Never use MD5, SHA-1, or SHA-256 for password storage. These algorithms are designed to be fast — precisely the wrong property for protecting passwords. Use BCrypt, Argon2, or scrypt instead.

How BCrypt Works

BCrypt was designed in 1999 specifically for password hashing. It has three key properties that make it suitable where SHA-256 is not:

1. It's Intentionally Slow

BCrypt uses a configurable cost factor (also called work factor or rounds). A cost factor of 10 means the algorithm performs 2¹⁰ = 1,024 iterations internally. Cost factor 12 means 2¹² = 4,096 iterations. Each iteration increase roughly doubles the time to hash — and doubles the time for an attacker to crack.

2. It Includes a Salt Automatically

BCrypt generates a unique random salt for every password hash. The salt is embedded in the output string — you don't need to store it separately. This means two identical passwords produce completely different hashes, which defeats precomputed rainbow table attacks.

3. The Output Is Self-Contained

A BCrypt hash looks like this:

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPj/Ur2aVvx5G

Breaking this down:

  • $2b$ — BCrypt version identifier
  • 12$ — cost factor (12 in this case)
  • Next 22 characters — the random salt
  • Remaining characters — the hash

Everything needed to verify a password is in that one string. No separate salt column needed in your database.

Choosing a Cost Factor

The right cost factor is one that makes hashing take approximately 100–300ms on your hardware. This is fast enough that users don't notice during login, but slow enough that brute-force attacks become impractical.

Cost FactorApprox. time (modern server)Recommendation
10~10msMinimum acceptable
12~40msGood default for most apps
14~160msHigh-security applications
16~640msVery high security (slow UX)

As hardware gets faster over time, you should increase your cost factor. BCrypt lets you re-hash passwords at login with a higher cost factor without invalidating existing hashes.

Using BCrypt in Code

// Node.js — bcrypt library
import bcrypt from 'bcrypt';

const COST_FACTOR = 12;

// Hashing a password (at registration)
async function hashPassword(plaintext) {
  return await bcrypt.hash(plaintext, COST_FACTOR);
}

// Verifying a password (at login)
async function verifyPassword(plaintext, hash) {
  return await bcrypt.compare(plaintext, hash);
}

// Usage
const hash = await hashPassword('user-password');
// Store hash in database — never store the plaintext

const isValid = await verifyPassword('user-password', hash);
// true — use this to grant access
# Python — bcrypt library
import bcrypt

COST_FACTOR = 12

# Hashing (at registration)
def hash_password(plaintext: str) -> bytes:
    return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt(rounds=COST_FACTOR))

# Verifying (at login)
def verify_password(plaintext: str, hashed: bytes) -> bool:
    return bcrypt.checkpw(plaintext.encode(), hashed)

BCrypt vs Argon2

Argon2 is the winner of the 2015 Password Hashing Competition and is considered the modern successor to BCrypt. The key difference: Argon2 is also memory-hard — it requires a configurable amount of RAM in addition to CPU time. This makes GPU-based cracking even more difficult, since GPUs have limited memory bandwidth per core.

FeatureBCryptArgon2
Designed19992015
Memory-hardNoYes
Configurable costYes (iterations)Yes (time + memory)
Widely supportedExcellentGood, growing
Use todayYes — still solidPreferred for new projects

For new projects, Argon2id (a hybrid variant) is the current best practice. For existing BCrypt implementations, there's no urgent need to migrate — BCrypt remains secure when used with a sufficient cost factor.

Test your implementation: The BCrypt Hash Generator lets you generate hashes at different cost factors and verify that plaintext matches a hash. Useful for understanding the output format, testing your verification logic, and checking that your stored hashes are valid during a migration.