MD5 vs SHA vs AES: Key Differences Between Hash and Encryption Algorithms
Overview
MD5, SHA, and AES appear frequently in development, but they serve completely different purposes. I see developers confuse them all the time — using MD5 for password storage, assuming SHA is encryption, or thinking AES is a hash function.
Here is the distinction in one sentence: MD5 and SHA are hashing algorithms (one-way, irreversible). AES is an encryption algorithm (two-way, reversible with a key).
If you remember nothing else from this article, remember that.
Hashing vs Encryption
Hashing
A hash function takes any input and produces a fixed-length output. The same input always produces the same output. You cannot reverse a hash back to the original input — that is the entire point.
import hashlib
# Same input always produces the same hash
text = "hello world"
md5_hash = hashlib.md5(text.encode()).hexdigest()
sha256_hash = hashlib.sha256(text.encode()).hexdigest()
print(f"MD5: {md5_hash}") # 32 hex chars
print(f"SHA256: {sha256_hash}") # 64 hex charsEncryption
Encryption transforms data using a key. With the key, you can reverse the transformation. Without the key, you cannot (assuming strong encryption).
from cryptography.fernet import Fernet
# Generate a key
key = Fernet.generate_key()
cipher = Fernet(key)
# Encrypt
plaintext = b"hello world"
ciphertext = cipher.encrypt(plaintext)
print(f"Encrypted: {ciphertext}")
# Decrypt (reversible with the key)
decrypted = cipher.decrypt(ciphertext)
print(f"Decrypted: {decrypted}") # b"hello world"MD5: Legacy Hash
MD5 produces a 128-bit (32 character) hex output. It was once the standard for file integrity checks.
Why MD5 Is Deprecated
MD5 is broken. Security researchers demonstrated collision attacks in 2004 — finding two different inputs that produce the same MD5 hash. By 2017, generating a collision took less than a dollar of cloud compute time.
# MD5 is fast but insecure
import hashlib, time
start = time.time()
for i in range(100000):
hashlib.md5(b"test").hexdigest()
print(f"100K MD5 hashes: {time.time()-start:.2f}s")Use MD5 only for non-security purposes: checksums for file deduplication, caching keys, or compatibility with legacy systems.
SHA-1, SHA-2, SHA-3
SHA-1 (Deprecated)
Also broken since 2017 (Google demonstrated a collision). Git still uses SHA-1 for commit hashes, but that is a different context — Git uses SHA-1 for content addressing, not security.
SHA-2 (Current Standard)
SHA-2 includes SHA-224, SHA-256, SHA-384, and SHA-512. SHA-256 is the most common — used in SSL/TLS certificates, Bitcoin, and Docker image verification.
// SHA-256 in Go
import (
"crypto/sha256"
"fmt"
)
func main() {
h := sha256.New()
h.Write([]byte("hello world"))
fmt.Printf("%x", h.Sum(nil))
}SHA-3 (Future-Proof)
SHA-3 is the newest NIST standard. It uses a completely different internal structure (Sponge construction) from SHA-2. If you are designing a new system today with a 20-year horizon, use SHA-3.
AES Encryption
AES (Advanced Encryption Standard) is the gold standard for symmetric encryption. Governments use it to protect classified information. Your HTTPS connection uses it. Your Wi-Fi password is protected by it.
AES Modes
AES operates in different modes. The most common:
// AES-GCM in Node.js
const crypto = require("crypto")
const key = crypto.randomBytes(32) // 256-bit key
const iv = crypto.randomBytes(12) // 96-bit IV for GCM
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv)
let encrypted = cipher.update("hello world", "utf8", "hex")
encrypted += cipher.final("hex")
const tag = cipher.getAuthTag().toString("hex")
console.log({ encrypted, tag })
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv)
decipher.setAuthTag(Buffer.from(tag, "hex"))
let decrypted = decipher.update(encrypted, "hex", "utf8")
decrypted += decipher.final("utf8")
console.log(decrypted) // "hello world"AES Key Sizes
AES supports three key sizes: 128, 192, and 256 bits. AES-128 is still secure. AES-256 provides a higher security margin. For most applications, AES-128 is sufficient. For compliance or paranoid workloads, use AES-256.
Real-World Mistakes I Have Seen
Mistake 1: Hashing Passwords with MD5 or Plain SHA
If your user database gets leaked, MD5 hashes are reversed in milliseconds. Use bcrypt, scrypt, or Argon2 instead:
import bcrypt
# Hash a password (bcrypt includes the salt automatically)
hashed = bcrypt.hashpw(b"user_password", bcrypt.gensalt())
# Verify
bcrypt.checkpw(b"user_password", hashed) # TrueMistake 2: Using ECB Mode
ECB mode encrypts each block independently. The result: images encrypted with ECB still show outlines of the original image because identical pixel values produce identical encrypted blocks.
Mistake 3: Hardcoding Keys
// Bad
const encryptionKey = "MySuperSecretKey123"
// Better
const encryptionKey = process.env.ENCRYPTION_KEYWhich Algorithm Should You Use?
Wrap Up
The hash vs encryption distinction is not academic — choosing the wrong algorithm creates security holes. Hash for verification, encrypt for confidentiality, and use the right tool for each job.