Hashing vs Encoding
Demystifying Cryptographic Primitives
One of the most common mistakes junior developers make is confusing Encryption, Hashing, and Encoding. They serve completely different purposes.
Encoding (Data Translation)
Encoding transforms data into a new format for system compatibility. It is not secure. There are no keys. Anyone who knows the encoding format can reverse it. - Goal: Usability and data transfer. - Example: Base64. If you need to send binary image data over a text-only JSON API, you encode the image into Base64 text. The receiver easily decodes it back to an image. - Rule: Never use encoding for security.
Encryption (Reversible Secrecy)
Encryption scrambles data using a secret key. It is meant to be reversed (decrypted) later by someone who holds the key. - Goal: Confidentiality. - Example: AES-256.
Hashing (Irreversible Fingerprinting)
Hashing takes an input of any size (a password, a 10GB movie file) and runs it through a mathematical algorithm to produce a fixed-size string of characters, called a hash digest. - Goal: Integrity and Verification. - Key Property 1 (One-Way): You cannot reverse a hash back into the original data. It is mathematically impossible. - Key Property 2 (Deterministic): The same input always produces the exact same hash. - Key Property 3 (Avalanche Effect): Changing even a single comma in a 10GB file completely changes the resulting hash.
Real-World Usage: Storing Passwords
You should never store user passwords in plaintext or encrypted format. If a hacker steals your database, they get everyone's passwords.
Instead, you Hash the passwords.
When a user signs up with the password "hunter2", you hash it to 74b87337... and save the hash.
When they try to log in later, you hash the password they typed and compare the two hashes. If the hashes match, the password is correct. You verified they know the password without ever storing the password itself!
import bcrypt
password = b"super_secret_password"
# 1. Hashing the password (incorporates a unique salt automatically)
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
print(f"Store this in DB: {hashed}")
# 2. Verifying a login attempt later
login_attempt = b"super_secret_password"
if bcrypt.checkpw(login_attempt, hashed):
print("Login Successful!")
else:
print("Invalid Password!")
Standard Hashing Algorithms: - SHA-256 / SHA-3: Excellent for file integrity checks and blockchain. - Bcrypt / Argon2: Specifically designed for passwords. They are intentionally slow to compute, making brute-force guessing attacks economically unviable for hackers.
Why Plain Hashes Are Not Enough for Passwords
Hashing a password with SHA-256 and storing the digest sounds safe, but it fails in practice for two reasons: rainbow tables and identical passwords. A rainbow table is a precomputed map of millions of common passwords to their SHA-256 digests; when attackers steal a database, they simply look up the digest and recover the password in microseconds. Worse, because SHA-256 is deterministic, two users sharing the password "summer2024" produce the same digest — one compromise reveals both, and a count of duplicate hashes leaks which accounts share passwords.
The fix is a salt: a per-user random value (usually 16 or 32 bytes) mixed into the hash. The stored digest becomes hash(password + unique_salt), so identical passwords produce entirely different digests, and each stolen account must be attacked independently. The salt is stored alongside the hash in plaintext; it exists to defeat rainbow tables and make each crack an individual effort, not to be secret.
import hashlib, os
salt = os.urandom(16)
digest = hashlib.pbkdf2_hmac("sha256", b"password", salt, 600_000)
# store salt + digest together; verify by recomputing with the stored salt
Password Hashes Have Their Own Family
Fast general-purpose hashes (MD5, SHA-1, SHA-256) are too fast for passwords — a modern GPU computes billions of SHA-256 operations per second. Password-hashing algorithms are deliberately slow and memory-hard, so each guess costs real time and RAM. Bcrypt stays popular but caps its input at 72 bytes. scrypt adds memory-hardness against ASIC attackers. Argon2id is the modern recommendation, offering tunable time-cost, memory-cost, and parallelism:
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
digest = ph.hash("correct horse battery staple")
assert ph.verify(digest, "correct horse battery staple")
When interacting with legacy systems you will also see PBKDF2 (the OWASP-recommended retro-fit), which keeps SHA-2 but stretches it through hundreds of thousands of iterations. For anything new, choose Argon2id; if you cannot, choose scrypt or bcrypt with a high work factor — never MD5, never SHA-1, never unsalted SHA-2. Use hashing for integrity and detection (file checksums, Git commits, password verification), and remember it is irreversible fingerprinting, not encryption.