Randomly

Hashes and encoding

MD5 vs SHA-256: checksums, collisions, and what browsers still compute

Bipul Kumar 8 min
MD5 vs SHA-256: checksums, collisions, and what browsers still compute
On this page 0%

I still meet code that stores user passwords as MD5 hex “because it is hashed.” I also meet build scripts that refuse a tarball because SHA-256 does not match, which is the job MD5 used to pretend to do. Same family of hex strings. Different promises. One of those promises has been broken in public for years.

I run both algorithms in the browser with Web Crypto where the browser allows it, and with a small MD5 implementation where it does not. The MD5 generator and the SHA-256 generator are for checksums and for teaching. They are not a password stack. This post is collisions, what a digest is for, leftover SHA-1, and when I still use CRC32 without apology.

Broken MD5 logo next to a SHA-256 digest of the same short file
Same file in, two hex strings out. Only one of those strings is still a respectable fingerprint.

What a hash is for

A cryptographic hash maps arbitrary bytes to a fixed digest. MD5 is 128 bits. SHA-256 is 256 bits. SHA-1 is 160 bits and I wish it would leave my tickets. You want three properties in the crypto sense: preimage resistance (hard to invert), second preimage (hard to find another input with the same digest), and collision resistance (hard to find any two inputs that match). Password storage wants a slow, salted KDF, not a fast hash. File integrity wants collision resistance against someone who can pick both files. “Did the download flip a bit on the wire” is a weaker question. People blur these until an incident report uses the word “encrypted” about an MD5.

Fast hashes are fast on purpose. You can hash a disk image in a time humans will wait. That also means an attacker can try billions of password guesses per second on MD5. If you need password hashing, stop reading this as a how-to and go use Argon2. If you need to fingerprint a fixture file, keep reading.

MD5 collisions are not theoretical

Researchers produced MD5 collisions in the 2000s. Chosen-prefix collisions got good enough to forge certificate pairs. Flame used that class of trick. I do not need to reenact the paper. I need you to stop using MD5 as proof that two files are the same when one of the files is chosen by someone else.

I still use MD5 when a vendor’s documentation says “compare MD5” and I cannot move the vendor. I treat a match as “this is probably the file they meant” for accidental corruption, not for malice. If the threat includes a person who can craft a pair of PDFs, MD5 is theater. SHA-256 is the default I print on a download page.

Git used SHA-1 for years and is moving. That migration exists because collisions became practical enough to worry a project that size. Your CMS upload form is not Git, but “we are small” is not a collision defense. Small projects copy files from strangers too.

Do not “double MD5” to fix it. Do not MD5 then SHA-1. Combining broken primitives as a sandwich is a hobby, not a design. If you can call SHA-256, call SHA-256.

Browser SubtleCrypto digest call producing a SHA-256 hex string from a UTF-8 encoded prompt
SubtleCrypto.digest with SHA-256. MD5 is not in that menu, which is a hint.

SubtleCrypto in the browser

crypto.subtle.digest("SHA-256", data) is the honest path. Feed it a Uint8Array. If your input is text, encode UTF-8 yourself. If you hash a JavaScript string by char codes, you will disagree with every server that hashed bytes. I have spent hours on “the hash is wrong” tickets that were Latin-1 versus UTF-8. The same UTF-8 footgun shows up in Base64 tools. Bytes are bytes. Text is a story about bytes.

SubtleCrypto does not offer MD5. Browsers refused to bless it. That is why our MD5 tool is a software implementation. It is fine for identifying a known file you already trust. It is a red flag if you are designing a new protocol and MD5 is the only primitive you can call. Design the protocol around SHA-256. Keep MD5 as a compatibility adapter.

SHA-1 is in some browsers’ SubtleCrypto and I still do not want it for new work. The SHA-1 generator exists because git objects, old AWS signatures, and legacy checksum fields still show up. Generate the digest to match a system you cannot change. Do not pick SHA-1 because the hex is shorter than SHA-256.

HMAC is a different button. Hashing a message is not authenticating it. If you needed a signature, you needed a key and HMAC or a real signature scheme. People “sign” webhooks with md5(body + secret). That is a confused HMAC. Use HMAC-SHA-256. I am not putting a webhook signer in the blog post. I am telling you the generator page is not that signer.

Checksums versus password hashing

Checksum: detect accidental change. CRC32 and MD5 both do this in practice for noise on a disk. SHA-256 does it with margin against people who can edit the file.

Integrity with an attacker: SHA-256 plus a way to learn the expected digest from a trusted place (HTTPS page, signed metadata). A digest sitting next to the file on the same sketchy mirror proves nothing. The attacker changes both.

Passwords: slow KDF, unique salt, parameters you can ratchet. MD5(password) is a museum exhibit. SHA-256(password) is only slightly less embarrassing. SHA-256(salt + password) is still fast. Fast is the bug.

I will hash a UUID in a demo if I am showing the hash tool. I will not hash a UUID “to make it more unique.” I said that in the UUID v4 versus v7 post and I am saying it again because it keeps appearing in pull requests.

Side-by-side MD5 and SHA-256 output fields for the same sample sentence
Run both on the same UTF-8 bytes if you are migrating a checksum field. Expect no agreement.

When CRC32 is enough

Zip files. Ethernet. PNG chunks. A cache key you compute over bytes you already trust because they came from your own disk. CRC32 is tiny and very fast and very public. Collisions are easy if someone wants them. I use it when the failure mode is “bit flip” or “wrong file version from our own build.” I do not use it when the failure mode is “a user uploaded two PDFs that should not look like the same document.”

People replace CRC32 with MD5 thinking they upgraded security. They upgraded the digest size and kept a broken collision story. If you are going to pay for a crypto hash, pay for SHA-256. If you are not, stay with CRC and be honest in the comment: “not for untrusted input.”

Bloom filters and hash maps use non-crypto hashes too. That is fine. Do not mix those with “we hashed the PII so it is anonymous.” A fast hash of an email is still a join key. See PII-safe test data if the real problem is that you should not have the email in the fixture.

Migration notes

If you stored MD5 of files in a database, add a SHA-256 column. Compute both during a transition. Serve SHA-256 to new clients. Keep MD5 until the last old client dies. Do not “upgrade” by hashing the MD5 with SHA-256 and storing that. You wanted a hash of the bytes, not a hash of a broken digest.

Hex versus Base64 for the digest is display. Hex is the usual. Base64 is shorter. Neither is encryption. If someone “hides” a file by showing only its MD5, they hid nothing.

Empty string hashes are useful test vectors. MD5 of empty is d41d8cd98f00b204e9800998ecf8427e. SHA-256 of empty is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. I keep those in unit tests. If a library returns something else for empty, the library is hashing undefined or a trailing newline you did not see.

Newlines are the other classic. Windows \r\n versus Unix \n changes the digest. Hash bytes after you normalize, or hash the file as stored and document which. Our tools hash exactly what is in the box. If you paste from Word, you get Word’s bytes after UTF-8 encoding, including the fancy quotes.

What I click on Randomly

Need a fingerprint for a string I am about to put in a fixture? SHA-256. Need to match a legacy CMS field labeled “md5”? MD5. Need to match a git blob for curiosity? SHA-1, and then I go wash my hands. All of those pages run in the tab. I do not want a hash service in the cloud for a 20-byte password someone might paste by accident. They should not paste passwords into a hash toy. They will. Local is less rude.

If the string is actually a secret you meant to store, you wanted a password manager and a KDF, not a blog tool. If the string is a card number, you wanted Luhn, not a hash. Different tabs. The card generator is not a hashing demo.

Frequently asked questions

Is SHA-256 broken?

Not in the way MD5 is. People will still misuse it as a password hash. The primitive is fine for integrity. The password story is still “use a KDF.”

Can I use MD5 for non-security checksums?

Yes, with eyes open. Accidental corruption, legacy fields, vendor checklists. Not certificates, not password tables, not “proof” against a hostile publisher.

Why does my hash not match openssl?

Newlines, UTF-16 from a Windows API, hex versus binary output, and hashing a filename instead of file contents. Print the byte length first. If the lengths differ, the algorithms never got a chance.

Should I hash PII to anonymize test data?

Usually no. Hashed emails are still identifying with a dictionary. Generate synthetic values instead. Start with random emails and leave production dumps on the production side of the wall.