Understanding Hashing vs. Encryption vs. Encoding
The critical differences between one-way hashing, reversible encryption, and data encoding — and the security implications of confusing them.
Introduction
Hashing, encryption, and encoding are often confused — sometimes with serious security consequences. Storing passwords as base64 “encoded” strings (which are trivially reversible) or using MD5 to “encrypt” sensitive data (it’s actually a hash — and a broken one) are mistakes that have caused massive data breaches. These three operations serve entirely different purposes, require different algorithms, and should never be substituted for one another.
flowchart LR
I["Input data"] --> H["Hashing (SHA-256, bcrypt)"]
I --> E["Encryption (AES, Fernet)"]
I --> C["Encoding (Base64, URL)"]
H --> H2["Fixed-length digest, one-way — cannot recover input"]
E --> E2["Ciphertext, reversible only with the key"]
C --> C2["Reformatted output, reversible by anyone, no key"]
Hashing: One-Way Fingerprinting
A hash function maps arbitrary input to a fixed-length digest. It is a one-way operation — you cannot recover the original input from the hash:
import hashlib, bcrypt
# Cryptographic hash (NOT for passwords)
data = b"sensitive-data"
sha256_digest = hashlib.sha256(data).hexdigest()
print(sha256_digest) # 64-char hex — no way to reverse to "sensitive-data"
# Password hashing (bcrypt — correct approach)
password = b"user-password-123"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed) # $2b$12$... — includes salt, work factor
# Verification
is_valid = bcrypt.checkpw(b"user-password-123", hashed) # True
is_valid = bcrypt.checkpw(b"wrong-password", hashed) # False
Use hashing for:
- Password storage (bcrypt, Argon2, scrypt — never MD5/SHA1/SHA256)
- Data integrity verification (SHA-256 checksums)
- HMAC signatures for webhook payloads
Never use:
- MD5 or SHA1 for passwords (rainbow tables, fast brute-force)
- SHA-256 alone for passwords (no salt, too fast — use bcrypt/Argon2)
Encryption: Two-Way Transformation
Encryption transforms plaintext into ciphertext using a key, and can be reversed (decrypted) with the appropriate key:
from cryptography.fernet import Fernet
import os
# Symmetric encryption (AES-128-CBC under the hood)
key = Fernet.generate_key() # Store this securely!
cipher = Fernet(key)
plaintext = b"Secret customer data: SSN 123-45-6789"
ciphertext = cipher.encrypt(plaintext)
print(ciphertext) # b'gAAAAAB...' — unintelligible without key
decrypted = cipher.decrypt(ciphertext)
print(decrypted) # b"Secret customer data: SSN 123-45-6789"
# Asymmetric encryption (RSA) — encrypt with public key, decrypt with private
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
ciphertext = public_key.encrypt(plaintext, padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), label=None
))
original = private_key.decrypt(ciphertext, padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), label=None
))
Use encryption for:
- Data at rest (database columns with sensitive PII)
- Data in transit (TLS)
- Secrets storage (AWS KMS, HashiCorp Vault)
Encoding: Reversible Representation Change
Encoding transforms data between representations without secrecy — it is trivially reversible and provides zero security:
import base64, urllib.parse
# Base64 encoding — anyone can decode this
data = "Secret: password123"
encoded = base64.b64encode(data.encode()).decode()
print(encoded) # "U2VjcmV0OiBwYXNzd29yZDEyMw=="
decoded = base64.b64decode(encoded).decode()
print(decoded) # "Secret: password123"
# URL encoding
url_encoded = urllib.parse.quote("hello world & special=chars")
print(url_encoded) # "hello%20world%20%26%20special%3Dchars"
Use encoding for:
- Transmitting binary data in text channels (base64 for email attachments)
- URL-safe characters (percent encoding)
- Data format conversion
Never confuse encoding with security — base64 is not encryption.
Decision Matrix
| Scenario | Correct Approach | Wrong Approach |
|---|---|---|
| Store user passwords | bcrypt/Argon2 | MD5, SHA256, base64 |
| Encrypt DB credit card column | AES-256 encryption | Hashing (can’t decrypt) |
| Verify file integrity | SHA-256 hash | Encryption (overkill) |
| Send binary in JSON | base64 encoding | Encryption |
| API webhook authentication | HMAC-SHA256 signature | base64 “signature” |
Key Takeaways
- Hashing is one-way and irreversible — use it for password storage (bcrypt/Argon2) and integrity verification (SHA-256 checksums).
- Encryption is two-way and reversible with the correct key — use it for data that must be retrieved in plaintext (PII, secrets).
- Encoding (base64, URL encoding) is reversible by anyone — it provides zero security and should never be used as a security measure.
- Never store passwords as SHA-256 hashes — they have no salt and are too fast to compute, enabling rainbow table and brute-force attacks.
- bcrypt work factor (rounds parameter) intentionally makes hashing slow — higher rounds = slower brute-force, choose based on acceptable latency.
Historical figures, architectures, and capabilities are for informational purposes only. Not technical, professional, legal, or financial advice. Sources: Research papers, developer documentation.