Cryptography for Beginners: How Encryption Actually Works

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.