Skip to content
devtoolbox.cc

MD5, SHA-1, SHA-256: which hash function should you use?

· 6 min read

Open the Hash tool

“Which hash should I use?” has a different answer depending on what the hash is for. Checking that a download isn’t corrupted, detecting duplicate files, signing API requests and storing passwords all use hash functions, and each one has different requirements. Pick MD5 for a password database and you have a breach; pick bcrypt for file checksums and you’ve wasted a lot of CPU.

This article explains what the common hash functions guarantee, where each one is still fine, and where it isn’t. The hash generator computes MD5, SHA-1, SHA-256, SHA-384 and SHA-512 side by side for text or files, in the browser, so you can check every example below.

What a hash function does

A cryptographic hash function turns input of any size into a fixed-size output, the digest:

SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
SHA-256("")    = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Three properties make it useful for security:

Property Meaning
Preimage resistance Given a digest, you can’t find an input that produces it
Second-preimage resistance Given an input, you can’t find a different input with the same digest
Collision resistance You can’t find any two different inputs with the same digest

It is also deterministic (same input, same digest) and shows an avalanche effect: change one bit of the input and about half the output bits flip.

SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256("Hello") = 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969

A hash is not encryption. There’s no key and no way back; you can only hash a candidate input and compare.

The common algorithms

Algorithm Digest size Hex length Status
MD5 128 bits 32 Broken: collisions found in seconds
SHA-1 160 bits 40 Broken: practical collision shown in 2017
SHA-256 256 bits 64 Secure; the default choice
SHA-384 384 bits 96 Secure; SHA-512 family truncated
SHA-512 512 bits 128 Secure; often faster than SHA-256 on 64-bit CPUs
SHA-3-256 256 bits 64 Secure; different internal design (Keccak)

The hex length is just the digest size divided by 4, because each hex digit encodes 4 bits (the number base converter shows why). The same digest can also be written in Base64, which is shorter: ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0= is SHA-256(“abc”) again. Subresource Integrity attributes (integrity="sha256-...") use that form, and the hash tool can output either.

Why MD5 and SHA-1 are “broken”

“Broken” has a precise meaning here: collisions can be produced on purpose.

  • MD5: in 2004, Xiaoyun Wang and colleagues published practical MD5 collisions. Within a few years researchers had created a rogue CA certificate from an MD5 collision, and the Flame malware (2012) used one to pass as signed Windows code. Today a collision takes seconds on a laptop.
  • SHA-1: in 2017, Google and CWI Amsterdam published SHAttered, two different PDF files with the same SHA-1. Browsers had already stopped trusting SHA-1 certificates, and Git has been adding support for SHA-256 repositories since.

Collision attacks matter wherever someone can prepare two inputs in advance: certificates, signed documents, software packages. Finding an input for a given digest (a preimage) is still impractical for both, which is why “MD5 is broken” doesn’t mean every MD5 use is dangerous.

Where MD5 is still fine

MD5 is acceptable when nobody benefits from creating a collision:

  • detecting accidental corruption in a file you downloaded from a source you already trust
  • deduplicating files or cache entries you control
  • non-security fingerprints: cache keys, ETags, partitioning data by key

For anything where an attacker supplies the input, such as file uploads, signatures or integrity checks of third-party content, use SHA-256. The hash generator labels MD5 and SHA-1 “not for security use” for this reason.

Checking a download

Publishers usually list a SHA-256 checksum next to the file. To verify it:

sha256sum ubuntu.iso                 # Linux
shasum -a 256 ubuntu.iso             # macOS
Get-FileHash ubuntu.iso              # Windows PowerShell (SHA-256 by default)

Compare the whole string, not just the first and last few characters. Be clear about what this proves: the file matches the checksum on the page. If an attacker controls the page, they control both. That’s why serious projects also sign their checksum files with GPG or Sigstore.

You can also drop the file into the hash tool: it reads the file locally and shows all digests at once.

Hashing with a secret: HMAC

To prove that a message came from someone who knows a secret, as webhook signatures and API request signing do, it’s tempting to write SHA-256(secret + message). Don’t. MD5, SHA-1 and SHA-256 are vulnerable to length extension: knowing hash(secret + message), an attacker can compute hash(secret + message + padding + extra) without knowing the secret.

HMAC (RFC 2104) was designed to prevent exactly this:

// Node.js
import { createHmac } from 'node:crypto';
createHmac('sha256', 'key').update('The quick brown fox jumps over the lazy dog').digest('hex');
// f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8

When verifying an HMAC, compare with a constant-time function (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python). A plain === can leak, through timing, how many leading characters matched. The hash tool has an HMAC mode for checking webhook signatures by hand.

Passwords need a different kind of hash

SHA-256 is designed to be fast: billions of hashes per second on a GPU. That’s great for checksums and terrible for passwords, because an attacker with a leaked database can try billions of guesses per second. Unsalted hashes are worse still, since identical passwords produce identical digests:

SHA-256("password") = 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8

That value is in every precomputed lookup table on the internet.

Password storage needs a slow, salted, memory-hard function. The OWASP recommendations, in order:

  1. Argon2id, the winner of the Password Hashing Competition
  2. scrypt
  3. bcrypt, still fine and very widely available (mind its 72-byte input limit)
  4. PBKDF2 with a high iteration count, when FIPS compliance requires it

These functions include a random salt per password and a tunable work factor, which you raise as hardware gets faster. Use your framework’s password library rather than calling them by hand.

Hashing in the browser

The Web Crypto API computes SHA hashes natively:

const data = new TextEncoder().encode('abc');
const digest = await crypto.subtle.digest('SHA-256', data);
const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');

It supports SHA-1, SHA-256, SHA-384 and SHA-512, but not MD5. Browsers left MD5 out on purpose. For MD5 you need a library; the hash generator uses @noble/hashes, an audited implementation.

Which one to pick

Task Use
Verify a download or build artifact SHA-256
Content-addressed storage, dedup SHA-256 (MD5 only for trusted data)
Sign webhooks or API requests HMAC-SHA256
Store passwords Argon2id, scrypt or bcrypt
Subresource Integrity (integrity=) SHA-384 or SHA-256, Base64
Legacy system that expects MD5 MD5, but not for anything an attacker controls

When in doubt, SHA-256, except for passwords, where the answer is never a plain hash. To compare algorithms on your own input, paste text or drop a file into the hash generator.

← All posts