bcrypt for Password Hashing Why Encryption Alone Isn’t Enough

Опубликовано
bcrypt for Password Hashing: Why Encryption Alone Isn't Enough 1
Реклама · УДАЛИТЬ?

If you’re storing user passwords with AES, RSA, or even SHA-256, you’re doing it wrong. Not slightly wrong — fundamentally wrong. This is the single most common security mistake in web development, and it has a straightforward fix: use a proper password hashing function like bcrypt.

Here’s why it matters, how bcrypt works, and what production-ready code looks like.

Why Passwords Need Special Treatment

Most cryptographic operations are designed to be reversible or fast. Encryption is reversible by design — that’s its entire purpose. SHA-256 and MD5 are fast, processing gigabytes per second. Both of those properties are catastrophic for passwords.

When an attacker gets your database, they get your password hashes. With encryption, if they find the key, they decrypt everything. With fast hashes like MD5 or SHA-256, they run a GPU-accelerated brute-force attack — modern hardware can test hundreds of billions of MD5 hashes per second. Your “complex” password is cracked in minutes.

Passwords need to be:

  1. Not reversible — even with the key or algorithm, you can’t get the plaintext back
  2. Slow to compute — deliberate slowness makes brute force attacks impractical
  3. Unique per user — identical passwords must produce different hashes

bcrypt satisfies all three. General-purpose hash functions and encryption do not.

Hashing vs Encryption vs Password Hashing

These are not interchangeable:

ApproachReversible?Fast?Safe for Passwords?
Encryption (AES, RSA)Yes — with keyДаНет
Fast hashing (MD5, SHA-256)НетYes (by design)Нет
Password hashing (bcrypt, Argon2id)НетNo (by design)Да

The reversibility of encryption is a deal-breaker: compromise the key and you compromise every password. Fast hashing is a deal-breaker too: speed enables brute force. Password hashing functions are engineered to be slow — and that’s the point.

How bcrypt Works

bcrypt, designed in 1999 by Niels Provos and David Mazières, does three things that matter:

1. Salting. Before hashing, bcrypt generates a random salt (16 bytes) and includes it in the hash output. Even if two users share the same password, their hashes are different. This defeats precomputed rainbow table attacks entirely.

2. Work factor (cost). bcrypt accepts a cost parameter (typically 10–14). Each increment doubles the computation time. At cost 12, hashing takes roughly 250–400ms on modern hardware. That’s imperceptibly slow for a login request — but it turns a billion-attempt brute force into a decades-long operation.

3. The output is self-contained. A bcrypt hash looks like $2b$12$... and encodes the algorithm version, cost factor, salt, and hash together. You don’t need a separate salt column. Store the whole string.

Production Code: Node.js and Python

Node.js (bcryptjs or bcrypt)

const bcrypt = require('bcrypt');

const SALT_ROUNDS = 12;

// Hash a password
async function hashPassword(plaintext) {
  return bcrypt.hash(plaintext, SALT_ROUNDS);
}

// Verify a password against a stored hash
async function verifyPassword(plaintext, storedHash) {
  return bcrypt.compare(plaintext, storedHash);
}

// Usage
const hash = await hashPassword('hunter2');
// Store `hash` in your database

const isValid = await verifyPassword('hunter2', hash);
// true

Python (bcrypt)

import bcrypt

COST = 12

def hash_password(plaintext: str) -> bytes:
    salt = bcrypt.gensalt(rounds=COST)
    return bcrypt.hashpw(plaintext.encode('utf-8'), salt)

def verify_password(plaintext: str, stored_hash: bytes) -> bool:
    return bcrypt.checkpw(plaintext.encode('utf-8'), stored_hash)

# Usage
hashed = hash_password('hunter2')
# Store hashed in your database

is_valid = verify_password('hunter2', hashed)
# True

Store the full hash string. Never store plaintext, never store the salt separately, never store the intermediate values.

Choosing a Work Factor

The right cost factor depends on your hardware. The goal: make each hash operation take 200–500ms on your production server. That’s fast enough for good UX, slow enough to frustrate attackers.

Current recommendation: cost 12 as a minimum, 14 for high-value accounts (admin, financial). Run a benchmark on your actual hardware:

// Node.js: benchmark different cost factors
const bcrypt = require('bcrypt');

for (let cost = 10; cost <= 14; cost++) {
  const start = Date.now();
  await bcrypt.hash('benchmark', cost);
  console.log(`Cost ${cost}: ${Date.now() - start}ms`);
}

If cost 12 takes under 100ms, increase it. If cost 14 takes over 1000ms, drop to 13. Revisit annually — hardware gets faster, and your cost factor should keep pace.

You can test bcrypt hashing interactively with the IO Tools bcrypt Hash Generator.

bcrypt vs Argon2id vs scrypt

bcrypt is battle-tested and widely supported. But it has a limitation: it’s not memory-hard. An attacker with specialized hardware (ASICs or FPGAs) can parallelize attacks more efficiently than with memory-hard alternatives.

АлгоритмMemory-HardParallelism ResistantРекомендация
bcryptНетPartialGood default; use cost ≥12
scryptДаPartialBetter than bcrypt, less tooling
Argon2idДаДаPreferred for new projects

For new projects: use Argon2id. It won the Password Hashing Competition (2015), is memory-hard, resists GPU and ASIC attacks, and is now in OWASP’s top recommendation. The API is nearly identical to bcrypt.

For existing projects: if you’re already on bcrypt with a sane cost factor, migrating isn’t urgent. Add it to your next major refactor.

Common Implementation Mistakes

Hashing the hash. Some developers hash the password client-side before sending, then hash it again server-side. The client-side hash becomes the “password.” You’re now hashing a fixed-length hex string, not a human-chosen password — you lose nothing by the double hash, but you gain nothing either, and you introduce confusion.

The 72-byte truncation problem. bcrypt silently ignores everything beyond 72 bytes. A 100-character password and a 72-character password that share the same first 72 bytes are identical to bcrypt. If your users set very long passwords, this is a silent security degradation. Mitigation: pre-hash with SHA-256 before passing to bcrypt — but only if you fully understand the implications, and document it clearly.

Weak work factor. Cost 10 was reasonable in 2011. In 2026, use at least 12. If your existing records use cost 10, you can upgrade transparently: after a successful login, re-hash the verified password with the new cost and store the updated hash.

Async matters. bcrypt is CPU-intensive. Always use the async API (as shown above) in Node.js to avoid blocking the event loop. Synchronous bcrypt in a Node server will make every other request wait.

Migrating from MD5/SHA to bcrypt

You can’t re-hash without the original plaintext. But you can migrate opportunistically:

  1. Add a password_hash column alongside the old password_md5 столбец
  2. On successful login (when you have the plaintext), bcrypt-hash it and store in password_hash, clear the old column
  3. After a migration period, users who haven’t logged in can be forced to reset their password
  4. Once password_md5 is empty for all users, drop the column

This is the standard approach and requires no downtime.

Суть

Encryption is for data you need to retrieve. Hashing is for data you need to verify. Passwords should be verified, not retrieved — which means encryption is the wrong tool.

bcrypt gives you salting, configurable slowness, and a self-contained hash format. It’s been the right answer for 25 years. Use it at cost 12 or higher, use Argon2id for new projects, and migrate off MD5 and SHA-256 as soon as you can.

Getting this wrong is not a hypothetical risk. Databases get breached. When they do, properly bcrypt-hashed passwords are computationally impractical to crack. MD5 hashes are cracked overnight.

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?