Cryptography is the mathematical foundation of all digital security. HTTPS, password storage, VPNs, digital signatures, and cryptocurrency all rely on cryptographic principles. You don’t need a math degree to understand the concepts — this guide explains the essentials in plain English with practical examples.
Symmetric Encryption: One Key
# Symmetric encryption uses the same key to encrypt and decrypt
# Like a padlock: same key locks and unlocks
# AES (Advanced Encryption Standard) - the modern standard:
# - AES-128: 128-bit key (secure for most purposes)
# - AES-256: 256-bit key (used by military, NSA approved)
# - Block cipher: encrypts data in 16-byte blocks
# Python example with AES-256:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# Generate random 256-bit key:
key = os.urandom(32) # 32 bytes = 256 bits
# Encrypt:
nonce = os.urandom(12) # 96-bit nonce (never reuse!)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, b"Secret message", b"associated data")
# Decrypt:
plaintext = aesgcm.decrypt(nonce, ciphertext, b"associated data")
print(plaintext) # b"Secret message"
# GCM mode provides BOTH encryption AND authentication (AEAD)
# Authentication ensures ciphertext was not tampered with
Asymmetric Encryption: Public and Private Keys
# Asymmetric encryption uses a KEY PAIR:
# - Public key: share with everyone
# - Private key: keep secret
# Anything encrypted with public key can only be decrypted with private key
# Use cases:
# - TLS/HTTPS: server's public key encrypts session key
# - SSH: authenticate without passwords
# - Digital signatures: prove authenticity of documents/code
# RSA example (Python):
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
# Generate key pair:
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Encrypt with public key:
ciphertext = public_key.encrypt(
b"Secret message",
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
# Decrypt with private key:
plaintext = private_key.decrypt(ciphertext,
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
Hashing: One-Way Functions
# Hash functions convert any input to a fixed-size digest
# Properties:
# - Deterministic: same input = same output
# - One-way: cannot reverse (cannot get input from hash)
# - Avalanche effect: tiny input change = completely different hash
# - Collision resistant: cannot find two inputs with same hash
# Hash functions and their uses:
import hashlib
msg = b"The quick brown fox"
print(hashlib.md5(msg).hexdigest()) # 37c4b87edffc5d198ff5a185cee7ee09 (BROKEN - do not use for security)
print(hashlib.sha1(msg).hexdigest()) # 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12 (BROKEN for collisions)
print(hashlib.sha256(msg).hexdigest()) # d9014c4624... (GOOD - use this)
print(hashlib.sha512(msg).hexdigest()) # 91ea1245... (BETTER)
# DO NOT use MD5 or SHA1 for security purposes
# Both have known collision attacks (two different inputs = same hash)
Password Hashing: bcrypt and Argon2
# NEVER store passwords with SHA256 or MD5 alone!
# These are FAST hashes - attackers can test billions per second
# Password hashing must be SLOW:
# - bcrypt: tunable work factor, widely supported
# - Argon2: winner of Password Hashing Competition, best current choice
# - scrypt: memory-hard, good alternative
# bcrypt (Python):
import bcrypt
password = b"MySecretPassword"
# Hash (salt is automatic):
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed) # $2b$12$...
# Verify:
if bcrypt.checkpw(password, hashed):
print("Password matches!")
# rounds=12 means 2^12 = 4096 iterations
# On modern hardware: ~250ms per check
# Fast for users, devastatingly slow for attackers
# Argon2 (Python - even better):
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
hash = ph.hash("MySecretPassword")
ph.verify(hash, "MySecretPassword") # True
TLS/HTTPS: Putting It All Together
# TLS handshake combines all crypto types:
# 1. Asymmetric (RSA/ECDH): Exchange a session key securely
# 2. Symmetric (AES-256-GCM): Encrypt actual data with session key
# 3. Hashing (SHA-384): Verify data integrity (HMAC)
# 4. Digital signatures (ECDSA): Authenticate the server's certificate
# Check TLS configuration:
# Test your website:
nmap --script ssl-enum-ciphers -p 443 yourdomain.com
# Online: ssllabs.com/ssltest/ (grade your TLS configuration)
# Bad TLS settings (disable these):
# - TLS 1.0 and 1.1 (deprecated, vulnerable)
# - RC4 cipher (broken)
# - Export-grade ciphers (weak by design)
# - NULL ciphers (no encryption!)
# Good configuration (nginx example):
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# ssl_prefer_server_ciphers off;
Wrap Up
Cryptography is the invisible shield protecting your data. The practical takeaway: use AES-256 for symmetric encryption, RSA-2048+ or ECDSA for asymmetric, SHA-256 for general hashing, and bcrypt/Argon2 for passwords. Never roll your own cryptography — use established libraries. And always keep TLS certificates and ciphers up to date.