Hashing vs Encryption: MD5, SHA-256, and AES Explained Simply
Three operations get lumped together in code reviews all the time: encoding, hashing, and encryption. They all turn readable data into something that looks scrambled, which is exactly why they get confused — and why "we Base64 the password before storing it" still shows up in real pull requests. They solve three different problems, and picking the wrong one is how breaches happen. This guide separates the three cleanly, then goes deep on the one that trips up the most teams: storing passwords safely.
Encoding vs hashing vs encryption
Start with the property that actually distinguishes them: whether the transformation is reversible, and whether a key is involved.
- Encoding (Base64, hex, URL-encoding) is reversible with no key. Its purpose is to make bytes survive transport — fit binary into a JSON string, an email, or a URL. It provides zero confidentiality. Anyone can reverse it in one line.
- Hashing (SHA-256, MD5, bcrypt) is one-way with no key. It produces a fixed-size digest and is designed so you cannot recover the input from the output.
- Encryption (AES, RSA) is reversible only with a key. Its purpose is confidentiality: authorized parties who hold the key can recover the original; nobody else can.
If you remember one thing: encoding is a costume, not a lock. A value that is "just Base64" is plaintext with extra steps. We unpack that failure mode in Base64 encoding explained.
What a hash actually guarantees
A cryptographic hash takes any input and produces a fixed-length digest. The same input always yields the same digest, and a one-character change rewrites the whole output — the avalanche effect:
SHA-256("hello") =
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256("Hello") =
185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
Try it with the SHA-256 Generator or the multi-algorithm Hash Generator. The security of a hash rests on two distinct properties that people constantly blur together:
- Preimage resistance — given a digest
H, you cannot find any input that hashes to it. This is the "you can't reverse a hash" property. - Collision resistance — you cannot find two different inputs that hash to the same digest.
MD5 and SHA-1 broke on collisions, not preimages
This distinction is not academic — it decides what a broken hash can and cannot be used for. MD5 and SHA-1 are shattered on collision resistance but their preimage resistance still holds. Two real-world attacks make it concrete:
- In 2012, the Flame espionage malware forged a Microsoft code-signing certificate using an MD5 chosen-prefix collision — letting malware present itself as a legitimate Windows Update. That is a collision attack: the attacker crafted two inputs with the same MD5.
- In 2017, Google and CWI's SHAttered attack produced two distinct PDF files with an identical SHA-1 digest — the first practical SHA-1 collision.
Because preimage resistance survives, you still cannot take a given MD5 and compute an input that produces it. That is why MD5 remains acceptable as a non-adversarial checksum (detecting accidental disk corruption, deduplicating files) yet must never touch signatures, certificates, or password storage, where an attacker is actively trying to forge a match. SHA-256 is the modern default for anything an adversary might attack.
Why you must never store passwords with SHA-256
Here is the counterintuitive part. SHA-256 is the right choice for integrity, but the wrong choice for passwords — and the reason is speed. SHA-256 and MD5 are fast hashes, built to digest gigabytes per second. A single modern GPU computes billions of SHA-256 guesses per second (tens of billions for MD5). That throughput is a feature for checksums and a catastrophe for password storage: when an attacker steals your user table, a fast hash lets them run offline dictionary and brute-force attacks against every stolen digest at that same billions-per-second rate. The salt does not slow this down — it only stops precomputed rainbow tables and shared-password shortcuts.
The fix is a deliberately slow, memory-hard password hash. OWASP's current Password Storage guidance lists Argon2id as the first choice, with scrypt and bcrypt as acceptable alternatives. These functions take a tunable amount of time and memory per hash — you deliberately spend, say, a quarter-second and tens of megabytes of RAM per login. A quarter-second is invisible to a real user but collapses an attacker's billions-per-second into a few thousand per second, and the memory cost blunts the GPU advantage that makes fast-hash cracking cheap.
One sharp edge worth knowing: bcrypt silently truncates input at 72 bytes. Everything past the 72nd byte is ignored, so a long passphrase can be weaker than you think, and a common workaround — SHA-256 hashing the password first to shorten it — introduces a null-byte truncation bug of its own if done naively. Argon2id has no such limit.
Salt vs pepper
A salt is a unique random value per hash, stored openly alongside the digest. It guarantees that two users with the same password get different stored hashes, which defeats rainbow tables and stops an attacker from cracking many accounts at once. A pepper is a single secret value applied to every password and stored separately from the database — in an environment variable or an HSM. The insight is where each lives: the salt is useless to keep secret and lives in the same row, while the pepper's whole value is that a database dump alone does not contain it.
Length-extension, and why HMAC exists
Plenty of developers reach for hash(secret + message) to authenticate an API request. With
SHA-256 that is broken. SHA-256, SHA-1, and MD5 all use the Merkle–Damgård
construction, and its digest is the internal state at the end of the input. An attacker who
sees one valid hash(secret + message) can resume from that state and compute a valid digest
for secret + message + extra — appending data and forging a new signature without ever
knowing the secret. This is the length-extension attack, and it is exactly the gap
HMAC was designed to close through its nested keyed construction. (SHA-3 and BLAKE2 are
not Merkle–Damgård and are immune to length extension.) When you need to prove a message came from a
known sender and was not altered, use HMAC, not raw hash(secret + message) — see the
HMAC Generator.
Encryption: symmetric, asymmetric, and authenticated
Encryption is the reversible-with-a-key operation, and it comes in two families:
- Symmetric uses one shared key to both encrypt and decrypt. AES is the dominant standard — fast, hardware-accelerated on virtually every CPU, and used for files, disks, databases, and TLS session data.
- Asymmetric uses a public/private keypair: encrypt with the public key, decrypt with the private one (RSA, ECC). This solves key distribution and underpins TLS handshakes and digital signatures.
The trap is thinking encryption alone protects you. Encryption without authentication is exploitable. AES in CBC mode with no integrity check is vulnerable to padding-oracle attacks: by submitting tampered ciphertext and watching whether the server returns a "bad padding" error versus some other error, an attacker can decrypt the data byte-by-byte without ever having the key. The defense is an AEAD mode such as AES-GCM, which binds encryption and authentication together so any tampering is rejected before decryption is trusted. Experiment with the AES Encryption tool, and if you are choosing a mode for real data, choose an authenticated one.
Choosing the right tool
| Goal | Use | Do not use |
|---|---|---|
| Store passwords | Argon2id (or scrypt/bcrypt) + per-user salt | SHA-256, MD5, or encryption |
| Verify a download / detect tampering | SHA-256 | MD5 (if an attacker is in scope) |
| Keep data secret but recoverable | AES-GCM (authenticated) | AES-CBC with no MAC |
| Prove a message's origin & integrity | HMAC | hash(secret + message) |
| Make bytes transport-safe | Base64 / hex encoding | Anything, as a security control |
Try the tools
Conclusion
Encoding, hashing, and encryption are not three flavors of the same thing. Encoding gives you zero security. Hashing gives you a one-way fingerprint — but which property is broken decides whether a hash is still usable (MD5 lost collision resistance, kept preimage resistance). Passwords need a slow hash (Argon2id) precisely because fast hashes hand attackers billions of guesses per second on a stolen dump. And encryption without authentication is only half a control — reach for AES-GCM, not raw AES-CBC. Get these distinctions right and you have sidestepped the most common and most damaging mistakes in application security.
Frequently Asked Questions
Why is SHA-256 recommended for downloads but banned for passwords?
Its speed is the reason for both. Fast hashing is what you want to fingerprint a large file quickly, but that same billions-per-second speed lets an attacker brute-force a stolen password table cheaply. Passwords need a deliberately slow, memory-hard hash like Argon2id.
Can a broken hash like MD5 still be reversed?
No. MD5 is broken on collision resistance — you can craft two inputs with the same digest — but its preimage resistance holds, so you still cannot compute an input for a given MD5. That is why it survives as a non-adversarial checksum but is unsafe for signatures or certificates.
What is the difference between a salt and a pepper?
A salt is unique per password and stored openly next to the hash; it defeats rainbow tables and stops one crack from breaking many accounts. A pepper is one secret applied to all passwords and stored separately from the database, so a database dump alone does not reveal it.
Why is hash(secret + message) not a secure signature?
SHA-256, SHA-1, and MD5 use the Merkle–Damgård construction, so their output is the internal state. An attacker can extend a valid digest to sign appended data without knowing the secret — the length-extension attack. Use HMAC instead.
Is Base64 a form of encryption?
No. Base64 is encoding: no key, no secrecy, reversible by anyone in one line. It exists to make binary data transport-safe, not to protect it. Treating a Base64 value as secured is a real and recurring security bug.