Pick the wrong hashing algorithm and you either waste CPU cycles on something too slow, or you expose your users to an attack that was documented fifteen years ago. The choice between MD5, SHA-256, and bcrypt isn't arbitrary — each exists for a different job, and using one where another belongs is a real security failure.
What Makes a Good Hash Function
Before comparing algorithms, it helps to know what properties actually matter.
Deterministic — the same input always produces the same output. Without this, hashes are useless for verification.
Fixed output size — regardless of input length, the digest is always the same number of bits. SHA-256 outputs 256 bits no matter if the input is one byte or one gigabyte.
Avalanche effect — a tiny change in input produces a completely different output. Changing a single bit should flip roughly half the output bits. This makes hashes useful for detecting even minor tampering.
One-way (preimage resistance) — given a hash output, it should be computationally infeasible to reconstruct the original input.
Collision resistance — it should be infeasible to find two different inputs that produce the same hash.
Speed vs slowness — this one's a tradeoff. For checksums and data integrity, fast is good. For password hashing, fast is catastrophically bad.
MD5: Broken, but Still Everywhere
MD5 produces a 128-bit (32 hex character) digest and was once widely used for everything from file integrity to password storage. The problem: it's broken for security purposes.
Collision attacks against MD5 are practical — researchers have demonstrated creating two different files with the same MD5 hash. That means an attacker can craft a malicious file that passes an MD5 checksum check. The MD5 collision vulnerabilities were fully demonstrated by 2008.
Where MD5 is still acceptable:
- Non-security checksums where speed matters and collision attacks aren't a threat model (comparing locally generated file hashes to detect accidental corruption).
- Legacy systems where migration isn't feasible right now.
- Hash table internals where cryptographic properties aren't needed.
Where MD5 is never acceptable:
- Verifying downloaded software (use SHA-256 from the official source).
- Digital signatures.
- Anything involving passwords — ever.
SHA-1: Also Deprecated
SHA-1 produces a 160-bit digest and replaced MD5 as the default for a while. It's also broken. Google published the first practical SHA-1 collision in 2017 (the SHAttered attack). Certificate authorities stopped issuing SHA-1 certificates in 2016. If you see SHA-1 in a codebase today, it's technical debt.
SHA-256 and SHA-512: General Purpose
The SHA-2 family — including SHA-256 and SHA-512 — is the current standard for general-purpose cryptographic hashing. No practical collision attacks exist against them.
SHA-256 (256-bit output) is the default choice for most tasks: file integrity verification, HMAC signatures, certificate fingerprinting, blockchain, and as a building block in other protocols.
SHA-512 (512-bit output) gives a larger margin against theoretical future attacks and is actually faster than SHA-256 on 64-bit hardware, because modern CPUs handle 64-bit word operations efficiently. The output is just twice as long.
// SHA-256 via Web Crypto API (browser)
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
sha256('hello').then(console.log);
// → 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
You can verify SHA-256 outputs instantly with the Hash Generator.
Why Fast Hashing Is Terrible for Passwords
Here's the core insight that trips up developers: the properties that make SHA-256 great for file hashing make it terrible for passwords.
SHA-256 is designed to be fast. On modern hardware, you can compute billions of SHA-256 hashes per second. An attacker with a GPU farm can brute-force an 8-character lowercase password in minutes.
The solution isn't a "stronger" fast hash. It's an algorithm that is intentionally slow — tunable to require significant computation per attempt, even on specialized hardware.
bcrypt, Argon2, and scrypt: Password Hashing
These algorithms are designed specifically for storing passwords. They have a configurable cost factor that you can increase as hardware gets faster, maintaining the same security margin over time.
bcrypt has been the standard since 1999. It's based on the Blowfish cipher and includes a cost factor (work factor) and a built-in salt.
import bcrypt
# Hashing (typically takes ~100ms at cost=12)
password = b"hunter2"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# Verification
bcrypt.checkpw(password, hashed) # → True
Argon2 won the Password Hashing Competition in 2015 and is the current recommendation. It has three variants: Argon2d (maximizes GPU/ASIC resistance by data-dependent memory access, but vulnerable to side-channel attacks), Argon2i (data-independent memory access to resist side-channel attacks, but weaker against GPU-based cracking), and Argon2id (the recommended hybrid that uses Argon2i for the first half and Argon2d for the second, balancing both threat models). It lets you tune memory usage, parallelism, and iterations independently.
scrypt is memory-hard, meaning it requires a large amount of RAM in addition to CPU time. This makes it expensive to run on ASICs or GPUs optimized for fast computation without much memory.
The Right Cost Factor
For bcrypt, cost factor 12 is a reasonable minimum as of 2025 — it targets around 100-250ms per hash on a modern server. For Argon2id, the OWASP password storage cheat sheet recommends: m=19456 KiB (19 MiB), t=2, p=1 as a minimum.
Salting: Why It Matters
A salt is a random value added to the input before hashing. Its purpose: prevent precomputed attacks.
Without salts, an attacker can precompute hashes for every common password and build a rainbow table. Given a database of unsalted MD5 hashes, cracking them is just a lookup.
With salts, even two users with the same password get different hashes. The attacker must brute-force each hash individually — orders of magnitude more work.
bcrypt, Argon2, and scrypt all handle salting automatically. You don't generate the salt separately; it's embedded in the output string. This is one reason to use these libraries rather than rolling your own.
The MD5 Password Mistake
This deserves direct emphasis: never use MD5 (or SHA-1, or raw SHA-256) to hash passwords for storage, even with a salt.
The problem isn't the salt — it's the speed. A salted MD5 still lets an attacker compute millions of guesses per second per hash. A salted bcrypt with cost=12 limits them to a few hundred guesses per second, total, on a modern GPU.
If you inherit a system using MD5 for passwords, migrate off it. The standard approach: on next login, if the stored hash matches the MD5 of the input, replace it with a bcrypt hash. Active users get migrated naturally; inactive accounts can be force-reset.
Quick Reference
| Algorithm | Output | Use For | Speed |
|---|---|---|---|
| MD5 | 128-bit | Legacy checksums only | Very fast |
| SHA-1 | 160-bit | Nothing new | Fast |
| SHA-256 | 256-bit | File integrity, HMAC, TLS | Fast |
| SHA-512 | 512-bit | Same as SHA-256, larger margin | Fast |
| bcrypt | 60-char string | Password storage | Intentionally slow |
| Argon2id | Variable | Password storage (preferred) | Intentionally slow |
| scrypt | Variable | Password storage, key derivation | Intentionally slow |
Encoding Hash Outputs
Hash functions produce raw bytes. For transmission or storage, those bytes are typically encoded as lowercase hex strings (two hex digits per byte) or as base64 (more compact). The Base64 Encoder can handle the conversion if you need to represent a SHA-256 digest as base64 — common in HMAC signatures and content security policies.
For a broader view of how hashing relates to encoding and encryption, see Encoding vs Encryption vs Hashing.
Try the Hash Generator
The Hash Generator on UtilityKit computes MD5, SHA-1, SHA-256, SHA-512, and several other digests client-side — nothing leaves your browser. Useful for verifying file checksums, generating HMAC test vectors, or just checking that your implementation matches a known output.