Skip to main content
Security

Hashing algorithms: MD5, SHA-2, bcrypt, and Argon2 are not interchangeable

Pick hashes by job: fast hashes for integrity, password-hashing functions for passwords, and no hand-rolled crypto substitutions when the threat model changes.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 4 min read
// The line that shows up in the breach post-mortem:
const hash = crypto.createHash("sha256").update(password).digest("hex");
// stored: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
//         ^ identical for every user who ever picked "password"

That compiles, passes your login tests, and ships. It's also the difference between a breach that costs a forced password reset and one where an attacker recovers most of your users' plaintext overnight. The problem isn't SHA-256 being weak — it's that SHA-256 is fast, and fast is precisely the wrong property for stored passwords.

The question is never "which hash is best?" It's "what am I hashing, and what attack am I slowing down?" For file integrity where an attacker can't replace the expected digest, use SHA-256 or SHA-512. For new password storage, use Argon2id. Use bcrypt only where compatibility forces it, and don't use MD5 or SHA-1 for any security decision. The OWASP password-storage cheat sheet recommends Argon2id and keeps current parameter guidance.

JobUseDo not use
Download checksumSHA-256 plus a trusted published digest/signatureMD5 as a tamper guarantee
Password storageArgon2id with a unique saltPlain SHA-256, even salted
Legacy password verificationbcrypt while planning migrationA forced reset with no migration path
Message authenticationHMAC-SHA-256sha256(secret + message)

Password hashes must be expensive

// Pseudocode: use a maintained Argon2 library, not this as a crypto implementation.
const encoded = await argon2id.hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });
const ok = await argon2id.verify(encoded, suppliedPassword);

The encoded string carries the algorithm, salt, and work parameters inline ($argon2id$v=19$m=19456,t=2,p=1$...). That's what makes gradual upgrades possible: on a successful login, verify against the stored parameters, then rehash with stronger ones and write the new value. You can watch that encoded format take shape with an Argon2 hash generator before wiring it into your auth code.

Encryption is reversible with a key. Hashing is one-way. Encrypting passwords so you can recover them later is a design bug wearing the costume of a convenience feature.

The mistake behind "we use SHA-256"

SHA-256 is built to be fast — a modern GPU computes billions per second. An attacker with a stolen database can therefore make enormous numbers of guesses cheaply. A salt stops precomputed rainbow tables and forces the attacker to attack each hash separately, but it does nothing to make each guess expensive. Argon2id and bcrypt are deliberately slow, and Argon2id also demands memory, which specifically kneecaps the GPU parallelism attackers rely on.

FAQ

Isn't adding a salt enough to make SHA-256 safe for passwords?

No. A salt defeats rainbow tables and stops two users with the same password sharing a hash. It does not change how fast the attacker can test a single guess — and against a fast hash, "fast" is the entire problem. You need a function that's slow by design, not a fast one with a random prefix.

Argon2id or bcrypt for a brand-new system?

Argon2id. It won the Password Hashing Competition and its memory-hardness resists GPU and ASIC cracking in a way bcrypt's fixed 4 KB working set doesn't. Reach for bcrypt only when a platform or library leaves you no Argon2 option.

Why does bcrypt seem to ignore the end of my long password?

Most bcrypt implementations truncate the input at 72 bytes, so anything past that character silently doesn't count toward the hash. If you accept long passwords or passphrases, either pre-hash with SHA-256 and base64 the result before bcrypt, or use Argon2id, which has no such limit.

Is MD5 ever acceptable?

For non-security work, yes — cache keys, sharding, deduplication, checksums where no adversary can tamper with the expected value. The moment the digest is a security boundary (integrity you must trust, anything password-adjacent), MD5 is disqualified by its practical collisions.

Can I just do sha256(secret + message) for authentication?

No — that construction is vulnerable to length-extension attacks against SHA-2. Use HMAC-SHA-256, which is designed for exactly this and is available in every standard crypto library.

Pick the primitive by purpose, store passwords with a maintained password-hashing library, and benchmark parameters on your own production hardware so "expensive" stays expensive for the attacker but tolerable for your login endpoint. Crypto names are not interchangeable labels.

Cover photo by Laura Gigch on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

SecurityCryptographyPasswords

Related articles

All articles