Guide
Choosing a hash function: MD5, SHA-1, SHA-256, and passwords
MD5 is broken, so use SHA-256 — and then someone stores passwords as SHA-256 and has made things barely better. "Broken" means different things for a checksum, a signature, and a password, and the right answer depends entirely on which of those you are doing.
What a hash gives you
A cryptographic hash takes input of any length and produces a fixed-size digest — 128 bits for MD5, 160 for SHA-1, 256 for SHA-256. Three properties are what make it useful, and they fail independently:
- Preimage resistance — given a digest, you cannot find an input that produces it.
- Second preimage resistance — given an input, you cannot find a different input with the same digest.
- Collision resistance — you cannot find any two inputs with the same digest.
Collision resistance is the weakest of the three and the first to fall, because the attacker gets to choose both inputs. It is also the one that matters for signatures and certificates, and the one that does not matter at all for a file checksum you are comparing against a value you already trust.
A fourth property, the avalanche effect, is what makes hashes feel magical: changing one bit of input changes about half the output bits, with no relationship you can work backwards from.
MD5 and SHA-1: what is actually broken
MD5 lost collision resistance in 2004. Today, finding two inputs with the same MD5 takes seconds on a laptop, and chosen-prefix collisions — where the attacker controls meaningful content in both files — are routine. This was used in the wild: the Flame malware forged a Microsoft code-signing certificate with an MD5 chosen-prefix collision.
SHA-1 followed. The SHAttered attack in 2017 produced two different PDFs with the same SHA-1 digest, and by 2020 chosen-prefix SHA-1 collisions were demonstrated at a cost of tens of thousands of dollars — cheap enough that browsers, certificate authorities, and Git have all moved on.
What neither of them lost is preimage resistance. Nobody can take an MD5 digest and reverse it to the original input. That is why MD5 and SHA-1 remain perfectly reasonable for:
- Detecting accidental corruption — a file transfer, a disk read, a backup.
- Cache keys and content addressing, where nobody is trying to trick you.
- Deduplication in a trusted dataset.
- Sharding and bucketing, where you want a uniform spread and nothing more.
And why neither belongs anywhere an adversary picks the input: digital signatures, certificates, software distribution integrity, or anything where "these two files are the same" is a security decision.
Git still uses SHA-1 for object IDs. It mitigates the risk with a hardened variant that detects known collision patterns, and a transition to SHA-256 is specified but not widely deployed. This is a good illustration of the distinction: for identifying objects in your own repository, collisions are a non-issue; for accepting objects from a hostile source, they are not.
What to use instead
- SHA-256 — the sane default. Part of the SHA-2 family, widely implemented, hardware-accelerated on modern CPUs, no practical attacks.
- SHA-512 — same family, larger digest. On 64-bit hardware it is often faster than SHA-256 because it works in 64-bit words.
SHA-512/256gives you a 256-bit digest at that speed, and is immune to length extension. - SHA-3 — a structurally different design (Keccak sponge) standardised as a hedge in case SHA-2 ever falls. Slower in software, and not needed unless you have a specific reason.
- BLAKE3 — much faster than SHA-2, parallelisable, with a tree structure that supports verified streaming. Excellent for checksums at volume; not a NIST standard, which matters in some compliance contexts.
For almost everything, SHA-256 is the answer and the interesting decision is elsewhere.
Length extension, and why HMAC exists
MD5, SHA-1, and SHA-256 share a Merkle–Damgård construction, and it leaks something non-obvious: the digest is the internal state at the end of the message. Given H(secret || message) and the length of the secret — but not the secret itself — an attacker can compute H(secret || message || padding || extra) for any extra they like.
So this authentication scheme is broken:
// broken: attacker can append to message and forge a valid tag
tag = sha256(secret + message)
HMAC is the fix, and it is not just "hash it twice" — it is a specific nested construction with two derived keys that is provably secure even on a hash with weaknesses. HMAC-SHA-256 is the standard choice; HMAC-MD5 is, somewhat counterintuitively, still not practically broken, though there is no reason to choose it.
const tag = crypto.createHmac('sha256', secret).update(message).digest();
SHA-3, BLAKE2, BLAKE3, and SHA-512/256 are not vulnerable to length extension by construction, so H(secret || message) is safe with them — but HMAC is still the interoperable answer, and reviewers will not have to think about which family you picked.
When comparing a computed tag against a submitted one, use a constant-time comparison — crypto.timingSafeEqual, hmac.compare_digest, subtle.timingSafeEqual. A normal === returns as soon as it finds a differing byte, and that timing difference is enough to recover the expected value one byte at a time.
Passwords: the property you want is slowness
Everything above optimises for speed. For password storage, speed is the vulnerability. If your database leaks, the attacker has the digests and unlimited offline attempts, and a GPU rig computes billions of SHA-256 hashes per second. A dictionary of common passwords falls in seconds; anything under about ten characters falls eventually.
Password hashing functions are deliberately expensive, with a tunable cost so you can keep raising it as hardware improves:
- Argon2id — the current recommendation, winner of the Password Hashing Competition. Tunable in time, memory, and parallelism; the
idvariant resists both side-channel and GPU attacks. - scrypt — memory-hard, which is what makes custom hardware expensive rather than just slow.
- bcrypt — older, extremely well-tested, still fine. Note its quirk: it silently truncates input at 72 bytes, so pre-hashing long passphrases (or the output of a password manager) matters.
- PBKDF2 — the weakest of the four, because it is not memory-hard and parallelises well on GPUs. Choose it when a compliance regime requires a FIPS-approved primitive, with a high iteration count.
All four incorporate a per-user random salt, and modern implementations store it in the output string alongside the cost parameters, so you save one field and verification is self-describing:
$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
│ │ │ │ └── digest
│ │ │ └── salt
│ │ └── cost parameters
│ └── version
└── algorithm
The salt defeats precomputation: without it, one rainbow table cracks every user in every breached database at once, and identical passwords produce identical digests, which leaks which accounts share one.
Tune the cost to your hardware, not to a number from a blog post. The usual target is 200–500ms per verification on your production machines — slow enough to make offline attacks expensive, fast enough that login does not feel broken and an attacker cannot use it to exhaust your CPU.
Picking one, by what you are doing
- Detecting accidental corruption — any of them. CRC32 if you only care about transmission errors and want it fast; SHA-256 if you want to stop thinking about it.
- Verifying a download against a published digest — SHA-256, from a source you trust independently of the download itself.
- Content addressing, cache keys, dedup — SHA-256, or BLAKE3 at volume. Truncating to 128 bits is fine here; below that, birthday collisions start to matter.
- Authenticating a message — HMAC-SHA-256, compared in constant time.
- Storing passwords — Argon2id, or bcrypt if that is what your framework gives you. Never a bare fast hash, salted or not.
- Generating a token or ID — not a hash at all. Use a CSPRNG directly:
crypto.randomBytes(32). Hashing a timestamp or a counter produces something that looks random and is entirely predictable.
Last updated 10 August 2026