X Xerobit

bcrypt Password Hashing — Why You Should Use bcrypt and How to Implement It

bcrypt is the standard password hashing algorithm for web applications. Learn why MD5 and SHA-256 are wrong for passwords, how bcrypt's work factor prevents brute-force...

Mian Ali Khalid · · 5 min read
Use the tool
Hash Generator
Generate MD5, SHA-1, SHA-256, and SHA-512 hashes client-side.
Open Hash Generator →

bcrypt is the default choice for password hashing because it’s slow by design — slowing attackers attempting to crack stolen hashes. Using MD5 or SHA-256 for passwords is a critical security mistake.

Generate random salts and test hashes with the Hash Generator.

Why not SHA-256 for passwords?

// ❌ Wrong: SHA-256 is fast — attackers can try billions/second
const hash = crypto.createHash('sha256').update('password').digest('hex');
// An RTX 4090 can crack simple passwords in minutes

// ❌ Wrong: Adding salt doesn't solve the speed problem
const hash = sha256(salt + password);
// Still crackable via GPU acceleration at billions of iterations/second

// ✅ Correct: bcrypt is intentionally slow (~100ms per hash)
const hash = await bcrypt.hash(password, 12);
// At cost factor 12: ~250ms/hash → attacker rate: ~4 hashes/second

bcrypt in Node.js

npm install bcrypt
# or the pure-JS alternative:
npm install bcryptjs
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;  // Work factor (log2 of iterations)

// Hash a password:
async function hashPassword(plaintext) {
  const hash = await bcrypt.hash(plaintext, SALT_ROUNDS);
  // "$2b$12$..." — includes algorithm, cost factor, salt, and hash
  return hash;
}

// Verify a password:
async function verifyPassword(plaintext, hash) {
  return bcrypt.compare(plaintext, hash);
  // Returns true/false — never extract/compare manually
}

// Usage:
const hash = await hashPassword('MySecurePassword123');
console.log(hash);  // "$2b$12$sAlTsAlTsAlT...hashHashHash"

const valid = await verifyPassword('MySecurePassword123', hash);
console.log(valid);  // true

const invalid = await verifyPassword('WrongPassword', hash);
console.log(invalid); // false

Node.js: storing user passwords

import bcrypt from 'bcrypt';
import { db } from './db.js';

const SALT_ROUNDS = 12;

async function registerUser(email, password) {
  // 1. Validate password strength before hashing:
  if (password.length < 8) throw new Error('Password too short');
  
  // 2. Hash:
  const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
  
  // 3. Store ONLY the hash — never the plaintext:
  const user = await db.users.create({
    email: email.toLowerCase(),
    passwordHash,
    createdAt: new Date(),
  });
  
  return user;
}

async function loginUser(email, password) {
  const user = await db.users.findByEmail(email.toLowerCase());
  
  // ✅ Always compare even if user not found (prevent timing attacks):
  const dummyHash = '$2b$12$invalidhashfortimingattackprevention...';
  const hash = user?.passwordHash ?? dummyHash;
  
  const valid = await bcrypt.compare(password, hash);
  
  if (!user || !valid) {
    throw new Error('Invalid email or password');
  }
  
  return user;
}

Python: bcrypt with passlib

pip install passlib[bcrypt]
from passlib.context import CryptContext

# Configure password hashing:
pwd_context = CryptContext(
    schemes=['bcrypt'],
    deprecated='auto',
    bcrypt__rounds=12,
)

def hash_password(password: str) -> str:
    """Hash a password for storage."""
    return pwd_context.hash(password)

def verify_password(plaintext: str, hashed: str) -> bool:
    """Verify a password against a stored hash."""
    return pwd_context.verify(plaintext, hashed)

def needs_rehash(hashed: str) -> bool:
    """Check if hash should be upgraded (e.g., after increasing rounds)."""
    return pwd_context.needs_update(hashed)

# Usage:
stored_hash = hash_password("MyPassword123")
assert verify_password("MyPassword123", stored_hash)   # True
assert not verify_password("WrongPassword", stored_hash)  # False

Choosing the right cost factor

Cost factorApprox. time/hashNotes
10~100msMinimum recommended
12~250msGood default (2026)
14~1 secondHigh-security applications
16~4 secondsMay time out web requests

Rule of thumb: Choose the highest factor that completes in ≤ 300ms on your server.

// Benchmark on your server:
const start = Date.now();
await bcrypt.hash('test', 12);
console.log(`Cost 12: ${Date.now() - start}ms`);

bcrypt vs Argon2

Argon2 (winner of the 2015 Password Hashing Competition) is generally preferred for new applications:

npm install argon2
import argon2 from 'argon2';

// Hash:
const hash = await argon2.hash('password', {
  type: argon2.argon2id,  // argon2id recommended
  memoryCost: 65536,       // 64 MB memory usage
  timeCost: 3,             // 3 iterations
  parallelism: 4,          // 4 parallel threads
});

// Verify:
const valid = await argon2.verify(hash, 'password');

Use bcrypt if you need broad platform support (many languages have well-tested bcrypt libraries). Use Argon2id for new applications where you control the full stack.


Related posts

Related tool

Hash Generator

Generate MD5, SHA-1, SHA-256, and SHA-512 hashes client-side.

Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.