Introduction

Password hashing is critical for security in all modern applications. Bcrypt is the industry standard for securely hashing passwords and has been for over 20 years. In this guide, you’ll learn why bcrypt is so important and how to implement it correctly.

Why Not Just Store Passwords as Plain Text?

🚨 What Happens in a Data Breach?

If passwords are stored in plain text:

  • Hackers get direct access to all accounts
  • Users who reuse passwords are threatened on other platforms
  • GDPR violations and large fines
  • Total loss of user trust

Examples from history:

  • LinkedIn 2012: 6.5 million passwords stolen in plain text
  • Adobe 2013: 153 million user accounts compromised
  • Yahoo 2013-2014: 3 billion accounts affected

βœ… The Solution: Hashing

Hashing converts passwords to a fixed-length string that cannot be converted back:

hash("password123") = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"

But common hash functions (MD5, SHA-256) are still unsafe for passwords!

Why Not MD5 or SHA-256?

Problem with Fast Hash Functions

MD5/SHA-256 are designed for speed:

  • MD5: ~500 million hashes/second
  • SHA-256: ~200 million hashes/second

Why this is dangerous:

Common password: "password123"
SHA-256: "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

With GPU: 200 million hashes/second
Time to test 100 million common passwords: < 1 second! ⚑

Hackers can easily brute-force passwords with rainbow tables and GPU clusters.

What is bcrypt?

Bcrypt is an adaptive hash function specifically designed for passwords:

Key Features

  1. Adaptive: Cost factor can be increased as hardware gets faster
  2. Intentionally slow: ~10-100 hashes/second (thousands of times slower)
  3. Built-in salt: Automatic salt generation
  4. Proven track record: Used since 1999

How bcrypt Works

bcrypt(password, cost_factor) = "$2a$10$N9qo8uLOickgx2ZMRZoMye..."
                              β”‚  β”‚  β”‚
                              β”‚  β”‚  └─ Salt (22 chars)
                              β”‚  └──── Cost factor (10 = 2^10 rounds)
                              └─────── Algorithm version ($2a$)

Cost factor 10 = 2^10 = 1,024 iterations Cost factor 12 = 2^12 = 4,096 iterations

Bcrypt vs Alternatives

MethodSpeedRecommended?Use Case
MD5500M/sec❌ NeverLegacy systems
SHA-256200M/sec❌ NeverFile verification, not passwords
bcrypt10-100/secβœ… YesPasswords (standard)
scryptVariesβœ… YesPasswords (more modern)
Argon2Variesβœ… YesPasswords (most secure)
PBKDF2Varies⚠️ OKLegacy compatibility

Implementation

Node.js/JavaScript

Use bcryptjs or bcrypt:

const bcrypt = require("bcryptjs");

// Hash password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash("myPassword", saltRounds);
// Result: $2a$10$N9qo8uLOickgx2ZMRZoMye...

// Verify password
const isMatch = await bcrypt.compare("myPassword", hashedPassword);
// Result: true

Complete example:

async function registerUser(email, password) {
  // Hash the password
  const saltRounds = 12; // Recommended 2024
  const hashedPassword = await bcrypt.hash(password, saltRounds);

  // Save to database
  await db.users.insert({
    email,
    password: hashedPassword, // Store hash, never plaintext!
  });
}

async function loginUser(email, password) {
  // Get user
  const user = await db.users.findOne({ email });
  if (!user) throw new Error("User not found");

  // Verify password
  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) throw new Error("Invalid password");

  return user;
}

Python

import bcrypt

# Hash password
password = b"myPassword"
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
# Result: b'$2b$12$...'

# Verify password
is_match = bcrypt.checkpw(password, hashed)
# Result: True

PHP

// Hash password
$password = 'myPassword';
$hashed = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

// Verify password
if (password_verify($password, $hashed)) {
    // Password matches
}

Choosing the Right Cost Factor

Cost factor determines how many iterations run:

Cost FactorIterationsTime (2024)Recommendation
416< 0.01s❌ Too fast
8256~0.1s⚠️ Acceptable
101,024~0.3sβœ… Standard
124,096~1sβœ… Recommended 2024
1416,384~4s⚠️ May be too slow

Recommendation 2024: Cost factor 12

Increase Cost Factor Over Time

// When user logs in, rehash with higher cost
async function rehashIfNeeded(user) {
  const currentCost = extractCostFactor(user.password);
  const recommendedCost = 12;

  if (currentCost < recommendedCost) {
    const newHash = await bcrypt.hash(user.plainPassword, recommendedCost);
    await db.users.update(user.id, { password: newHash });
  }
}

Best Practices

βœ… Do This

  1. Use at least cost factor 10-12

    const hashed = await bcrypt.hash(password, 12);
  2. Always verify with compare()

    const isValid = await bcrypt.compare(inputPassword, storedHash);
  3. Rehash periodically

    • Increase cost factor as hardware gets faster
    • Rehash when users log in
  4. Combine with other security measures

    • Rate limiting on login
    • 2FA for sensitive accounts
    • Password strength requirements

❌ Don’t Do This

  1. Never store passwords in plain text

    // ❌ NEVER!
    await db.users.insert({ password: plainText });
  2. Don’t use fast hash functions

    // ❌ NEVER!
    const hash = sha256(password);
  3. Don’t create custom hashing logic

    // ❌ NEVER!
    function myHash(password) {
      /* custom logic */
    }
  4. Don’t expose cost factor

    • Bcrypt hash contains cost factor (OK)
    • But don’t expose implementation details

Security Considerations

Salt Handling

Bcrypt handles salt automatically:

  • Each hash has unique salt
  • Salt is included in hash string
  • No need for separate salt storage
const hash1 = await bcrypt.hash("password", 10);
// $2a$10$salt1...hash1

const hash2 = await bcrypt.hash("password", 10);
// $2a$10$salt2...hash2 (different salt, different hash!)

// Both verify correctly:
await bcrypt.compare("password", hash1); // true
await bcrypt.compare("password", hash2); // true

Timing Attacks

bcrypt.compare() is not fully secure against timing attacks:

// ⚠️ Possible timing attack
const user = await db.users.findOne({ email });
if (!user) throw new Error("User not found");
const isValid = await bcrypt.compare(password, user.password);

// βœ… Better: Even timing regardless of user existence
const user = await db.users.findOne({ email });
const hash = user?.password || "$2a$10$dummy";
const isValid = await bcrypt.compare(password, hash);
if (!user || !isValid) throw new Error("Invalid credentials");

Verify Your Implementations

Use our tools:

Migration from Unsafe Methods

Step 1: Identify Old Hashes

function isOldHash(hash) {
  // MD5: 32 hex chars
  if (/^[a-f0-9]{32}$/i.test(hash)) return true;

  // SHA-256: 64 hex chars
  if (/^[a-f0-9]{64}$/i.test(hash)) return true;

  // bcrypt: starts with $2
  if (/^\$2[ayb]\$/.test(hash)) return false;

  return true; // Unknown format
}

Step 2: Rehash on Login

async function login(email, password) {
  const user = await db.users.findOne({ email });
  if (!user) throw new Error("Invalid credentials");

  let isValid = false;

  if (isOldHash(user.password)) {
    // Verify against old hash
    isValid = sha256(password) === user.password;

    if (isValid) {
      // Rehash with bcrypt
      const newHash = await bcrypt.hash(password, 12);
      await db.users.update(user.id, { password: newHash });
    }
  } else {
    // Verify with bcrypt
    isValid = await bcrypt.compare(password, user.password);
  }

  if (!isValid) throw new Error("Invalid credentials");
  return user;
}

Conclusion

Bcrypt is the industry standard for password hashing for good reasons:

  • Secure: Adaptive and slow design
  • Proven: Used for over 20 years
  • Simple: Automatic salt handling
  • Scalable: Cost factor can be increased

When to use bcrypt:

  • βœ… Password hashing
  • βœ… API keys
  • βœ… Session tokens
  • βœ… Sensitive secrets

When to use something else:

  • SHA-256 for file verification
  • Argon2 for new projects (even more secure)

Next Steps