Encryption vs Hashing
Encryption is reversible (lock and unlock with a key); hashing is one-way (no way back). Learn when to use each, why passwords must always be hashed, and how salt and slow hashing defeat brute-force attacks.
Both encryption and hashing scramble your data into unreadable gibberish—which is exactly why they get confused. But the entire difference hinges on one word: reversible.
- Mental model: Encryption is a lock-and-key safe (data goes in, right key gets it back unchanged); hashing is a one-way blender (data goes in, you get a fixed-size digest you cannot un-blend).
Encryption: Two-Way, Key-Driven
Encryption takes your plaintext and scrambles it with a key into ciphertext. Hand over the right key, and you get the exact original back, character for character. No guessing, no approximation.
You reach for encryption whenever you need to retrieve the data later:
- Messages in transit (HTTPS, Signal, WhatsApp)
- Files at rest (encrypted hard drives, S3 buckets with server-side encryption)
- Database columns holding sensitive information
- API requests that must stay confidential
The critical insight: the security lives in the key, not the algorithm. AES-256 is public, battle-tested, open-source. Anyone can read the spec. But if they don't have your key, the ciphertext is useless noise. This is why key management—rotation, access control, hardware security modules—is the entire game.
Purpose: Confidentiality. Someone with the key reads everything; someone without it reads nothing.
Hashing: One-Way, Deterministic
Hashing takes any input—a password, a file, a sentence—and produces a fixed-size digest. That digest cannot be reversed. By design, there is no key and no undo button.
The magic: the same input always produces the same hash. Feed SHA-256 the string "hello" a thousand times, you get the same 64-character hex string every time. This determinism is what makes verification possible without storing the original.
Purpose: Verification and integrity. You don't need the original; you just need to check if two things match or if something was tampered with.
The Standard Wrong Way: Plaintext Passwords
Never store passwords in plaintext. If your database leaks, attackers get every password in seconds. Even worse: users reuse passwords across sites, so a breach at your company becomes a breach at their email, bank, and cloud storage.
The Right Way: Hash + Salt
Store the hash of the password, not the password itself. At login:
# Registration
stored_hash = hash_function(user_password)
# Save stored_hash to database
# Login
incoming_hash = hash_function(user_typed_password)
if incoming_hash == stored_hash:
print("Login successful")
else:
print("Wrong password")
But plain hashing is still vulnerable to brute force: if an attacker has your database dump, they can try millions of common passwords offline until one matches. You defeat this with:
- Salt: a random string appended to each password before hashing. Each user gets a different salt, so two users with the same password produce different hashes. Rainbow tables (precomputed hash tables) become useless.
- Slow hash function: bcrypt, scrypt, or argon2 deliberately burn CPU or memory on each hash computation. Trying a million passwords takes hours or days instead of seconds.
import bcrypt
# Registration
password = "MySecurePassword123"
salt = bcrypt.gensalt(rounds=12) # Cost factor: 12 rounds
hashed = bcrypt.hashpw(password.encode(), salt)
# Save hashed to database
# Login
incoming = "MySecurePassword123"
if bcrypt.checkpw(incoming.encode(), hashed_from_db):
print("Login successful")
else:
print("Wrong password")
Why argon2 over bcrypt? Argon2 (the OWASP recommended default as of 2024) adapts to modern hardware—it uses memory and parallelism in addition to time, making GPU and ASIC attacks harder. But bcrypt remains solid if you set the cost factor high enough (12+).
The Test
Can you get the original back? If yes, it's encryption. If no (by mathematical design), it's hashing.
| Property | Encryption | Hashing |
|---|---|---|
| Reversible? | Yes (with the key) | No, by design |
| Requires a key? | Yes | No |
| Fixed output size? | No (output = input size) | Yes (e.g., SHA-256 = 256 bits) |
| Same input → same output? | No (IV/nonce randomizes it) | Yes, always |
| Purpose | Confidentiality (read later) | Verification (check, don't retrieve) |
| Use for passwords? | No | Yes |
Passwords: The Cardinal Rule
Always hash passwords. Never encrypt them.
If you encrypt passwords, you must keep a key somewhere on your servers. If that key is compromised—or if a developer with access to it turns malicious—every password in your database becomes readable plaintext. You've only pushed the vulnerability sideways.
With hashing, there is no key to steal. Even if an attacker gets the database dump and all your code, they cannot reverse a bcrypt hash without spending months or years on a single password. And they'll hit the salt and slowness wall on every attempt.
One more reason: if your users reuse passwords (they do), encrypting them means you're actively hiding the fact that your system is compromised. They won't rotate their passwords on other services. Hashing forces transparency: if your database leaks, the plaintext passwords are mathematically unreachable, and you can confidently tell users their passwords are safe even though their account details were exposed.
One-Line Verdict
Reach for encryption when you need to retrieve and read the original data later (messages, files, database columns); reach for hashing when you only need to verify a match or detect tampering (passwords, checksums, deduplication). Passwords always get hashed, never encrypted.
Watch the 90-second reel on software-engineer-blog.com for the visual breakdown.