Cryptography Basics
Introduction
Cryptography is the practice and study of techniques for secure communication in the presence of adversaries. It's the mathematical foundation that enables confidentiality, integrity, and authenticity in digital systems. From HTTPS securing your web browsing to encrypted messaging apps, cryptography is everywhere in modern computing.
This reading covers the essential cryptographic concepts every developer needs to understand: symmetric and asymmetric encryption, hashing, common algorithms like AES and RSA, and digital signatures. You don't need to be a cryptographer to use cryptography correctly, but you do need to understand the basics to avoid dangerous mistakes.
Learning Objectives
By the end of this reading, you will be able to:
- Distinguish between symmetric and asymmetric encryption and when to use each
- Understand cryptographic hashing and its applications
- Use AES for symmetric encryption correctly
- Understand RSA and public key cryptography
- Implement digital signatures for authentication and non-repudiation
- Recognize common cryptographic mistakes and how to avoid them
- Apply cryptographic primitives to solve real-world security problems
Cryptographic Primitives
Cryptography provides three main types of operations:
- Encryption: Making data unreadable to unauthorized parties
- Hashing: Creating fixed-size fingerprints of data
- Digital Signatures: Proving authenticity and integrity
Let's explore each in detail.
Cryptographic Hashing
A cryptographic hash function takes input of any size and produces a fixed-size output (the hash or digest). Good hash functions are:
- Deterministic: Same input always produces same output
- Fast to compute: Efficient for any size input
- Irreversible: Cannot derive input from output (one-way function)
- Collision-resistant: Hard to find two inputs with same output
- Avalanche effect: Small input change completely changes output
Common Hash Functions
- SHA-256: 256-bit output, part of SHA-2 family, widely used
- SHA-3: Newest standard, different design from SHA-2
- BLAKE2: Fast, secure alternative to SHA-2
- MD5: BROKEN - Do not use for security (only for checksums)
- SHA-1: DEPRECATED - Being phased out, vulnerabilities known
Hashing Examples
import hashlib
# SHA-256 hashing
def hash_data(data):
"""Create SHA-256 hash of data"""
if isinstance(data, str):
data = data.encode('utf-8')
hash_object = hashlib.sha256(data)
return hash_object.hexdigest()
# Example usage
message = "Hello, World!"
hash_value = hash_data(message)
print(f"SHA-256: {hash_value}")
# Output: SHA-256: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
# Small change in input completely changes output (avalanche effect)
message2 = "Hello, World?"
hash_value2 = hash_data(message2)
print(f"SHA-256: {hash_value2}")
# Completely different hash despite only one character change
Use Cases for Hashing
1. Password Storage (with salt and key derivation)
import hashlib
import os
# BAD: Never store passwords like this!
def store_password_bad(password):
# Plain MD5 is completely insecure for passwords
return hashlib.md5(password.encode()).hexdigest()
# BETTER: Use salt and multiple iterations
def store_password_better(password):
# Generate random salt
salt = os.urandom(32) # 32 bytes = 256 bits
# Use PBKDF2 with many iterations (slow is good for passwords!)
iterations = 100000
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations
)
# Store salt and hash together
return salt + key
def verify_password(stored_password, provided_password):
# Extract salt (first 32 bytes)
salt = stored_password[:32]
# Extract hash (remaining bytes)
stored_key = stored_password[32:]
# Hash the provided password with same salt
key = hashlib.pbkdf2_hmac(
'sha256',
provided_password.encode('utf-8'),
salt,
100000
)
# Compare in constant time to prevent timing attacks
return hmac.compare_digest(key, stored_key)
# BEST: Use specialized password hashing libraries
import bcrypt
def hash_password(password):
# bcrypt automatically handles salting and uses adaptive cost
salt = bcrypt.gensalt(rounds=12) # Higher = slower = more secure
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed
def verify_password(password, hashed):
return bcrypt.checkpw(password.encode('utf-8'), hashed)
# Example
password = "my_secure_password"
hashed = hash_password(password)
print(f"Hashed: {hashed}")
# Verify
is_valid = verify_password("my_secure_password", hashed)
print(f"Password valid: {is_valid}") # True
is_valid = verify_password("wrong_password", hashed)
print(f"Password valid: {is_valid}") # False
2. Data Integrity Verification
import hashlib
import json
def create_checksum(file_path):
"""Create SHA-256 checksum of a file"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
# Read file in chunks to handle large files
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def verify_file_integrity(file_path, expected_checksum):
"""Verify file hasn't been tampered with"""
actual_checksum = create_checksum(file_path)
return actual_checksum == expected_checksum
# Example: Verifying downloaded file
expected = "abc123def456..." # From trusted source
if verify_file_integrity("downloaded_file.zip", expected):
print("File integrity verified")
else:
print("WARNING: File may be corrupted or tampered with!")
3. Hash-based Message Authentication Code (HMAC)
import hmac
import hashlib
def create_hmac(message, secret_key):
"""Create HMAC for message authentication"""
if isinstance(message, str):
message = message.encode('utf-8')
if isinstance(secret_key, str):
secret_key = secret_key.encode('utf-8')
return hmac.new(secret_key, message, hashlib.sha256).hexdigest()
def verify_hmac(message, signature, secret_key):
"""Verify HMAC signature"""
expected_signature = create_hmac(message, secret_key)
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(signature, expected_signature)
# Example: API request signing
secret_key = "your-secret-key-here"
message = "GET /api/users?page=1"
# Sender creates signature
signature = create_hmac(message, secret_key)
# Receiver verifies signature
is_valid = verify_hmac(message, signature, secret_key)
print(f"Signature valid: {is_valid}")
Symmetric Encryption
Symmetric encryption uses the same key for both encryption and decryption. It's fast and efficient for encrypting large amounts of data.
Characteristics
- Same key for encryption and decryption
- Fast - suitable for large data volumes
- Key distribution problem - how do both parties get the key securely?
AES (Advanced Encryption Standard)
AES is the gold standard for symmetric encryption. It's used everywhere from file encryption to VPNs to secure messaging.
Key sizes:
- AES-128: 128-bit key (16 bytes)
- AES-192: 192-bit key (24 bytes)
- AES-256: 256-bit key (32 bytes)
Modes of operation:
- ECB (Electronic Codebook): NEVER USE - insecure, patterns visible
- CBC (Cipher Block Chaining): Common but requires padding
- GCM (Galois/Counter Mode): RECOMMENDED - provides encryption + authentication
- CTR (Counter Mode): Good for parallel processing
AES-GCM Example
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
class SecureEncryption:
def __init__(self):
# Generate a random 256-bit key (keep this secret!)
self.key = AESGCM.generate_key(bit_length=256)
def encrypt(self, plaintext, associated_data=None):
"""
Encrypt plaintext using AES-256-GCM
Args:
plaintext: Data to encrypt
associated_data: Additional authenticated data (not encrypted)
Returns:
tuple: (nonce, ciphertext)
"""
if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
if associated_data and isinstance(associated_data, str):
associated_data = associated_data.encode('utf-8')
# Generate random nonce (number used once)
# CRITICAL: Never reuse a nonce with the same key!
nonce = os.urandom(12) # 96 bits for GCM
# Create cipher instance
aesgcm = AESGCM(self.key)
# Encrypt and authenticate
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
return nonce, ciphertext
def decrypt(self, nonce, ciphertext, associated_data=None):
"""
Decrypt ciphertext using AES-256-GCM
Args:
nonce: The nonce used during encryption
ciphertext: Encrypted data
associated_data: Additional authenticated data
Returns:
bytes: Decrypted plaintext
Raises:
cryptography.exceptions.InvalidTag: If authentication fails
"""
if associated_data and isinstance(associated_data, str):
associated_data = associated_data.encode('utf-8')
aesgcm = AESGCM(self.key)
# Decrypt and verify authentication tag
plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data)
return plaintext.decode('utf-8')
# Example usage
crypto = SecureEncryption()
# Encrypt sensitive data
secret_message = "This is confidential information"
nonce, ciphertext = crypto.encrypt(secret_message)
print(f"Encrypted (hex): {ciphertext.hex()}")
# Decrypt
decrypted = crypto.decrypt(nonce, ciphertext)
print(f"Decrypted: {decrypted}")
# With associated data (authenticated but not encrypted)
nonce, ciphertext = crypto.encrypt(
"account_number: 12345",
associated_data="user_id: 42"
)
# Must provide same associated data to decrypt
decrypted = crypto.decrypt(nonce, ciphertext, associated_data="user_id: 42")
Common Mistakes with Symmetric Encryption
# MISTAKE 1: Using ECB mode
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# BAD: ECB reveals patterns in plaintext
def encrypt_ecb_bad(plaintext, key):
cipher = Cipher(
algorithms.AES(key),
modes.ECB(), # NEVER USE THIS!
backend=default_backend()
)
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
# MISTAKE 2: Reusing IV/nonce
# BAD: Using same IV for multiple encryptions
static_iv = b'0' * 16 # NEVER DO THIS!
def encrypt_bad(plaintext, key):
cipher = Cipher(algorithms.AES(key), modes.CBC(static_iv))
encryptor = cipher.encryptor()
return encryptor.update(plaintext) + encryptor.finalize()
# MISTAKE 3: No authentication (encryption without integrity)
# Use AEAD modes like GCM instead of plain CBC/CTR
# MISTAKE 4: Hardcoded keys
SECRET_KEY = b'hardcoded_key123' # NEVER HARDCODE KEYS!
Asymmetric Encryption (Public Key Cryptography)
Asymmetric encryption uses two related keys: a public key (can be shared) and a private key (must be kept secret). Data encrypted with one key can only be decrypted with the other.
Characteristics
- Key pair: Public and private key
- Slow: Much slower than symmetric encryption
- Key distribution: Solves the key exchange problem
- Common uses: Key exchange, digital signatures, authentication
RSA (Rivest-Shamir-Adleman)
RSA is the most widely-used public key algorithm.
Key sizes:
- 2048 bits: Minimum recommended
- 3072 bits: Better security margin
- 4096 bits: High security (slower)
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
class RSAEncryption:
def __init__(self, key_size=2048):
# Generate RSA key pair
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size
)
self.public_key = self.private_key.public_key()
def encrypt(self, plaintext):
"""Encrypt with public key"""
if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
ciphertext = self.public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return ciphertext
def decrypt(self, ciphertext):
"""Decrypt with private key"""
plaintext = self.private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return plaintext.decode('utf-8')
def export_public_key(self):
"""Export public key in PEM format"""
pem = self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return pem.decode('utf-8')
def export_private_key(self, password=None):
"""Export private key in PEM format (optionally encrypted)"""
encryption = serialization.NoEncryption()
if password:
if isinstance(password, str):
password = password.encode('utf-8')
encryption = serialization.BestAvailableEncryption(password)
pem = self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=encryption
)
return pem.decode('utf-8')
# Example usage
rsa_crypto = RSAEncryption()
# Encrypt with public key (anyone can do this)
message = "Secret message"
encrypted = rsa_crypto.encrypt(message)
print(f"Encrypted: {encrypted.hex()[:64]}...")
# Decrypt with private key (only private key holder can do this)
decrypted = rsa_crypto.decrypt(encrypted)
print(f"Decrypted: {decrypted}")
# Export keys
public_pem = rsa_crypto.export_public_key()
print("Public Key:")
print(public_pem)
private_pem = rsa_crypto.export_private_key(password="strong_password")
print("\nPrivate Key (encrypted):")
print(private_pem[:100] + "...")
Hybrid Encryption
In practice, we combine symmetric and asymmetric encryption:
- Generate random symmetric key
- Encrypt data with symmetric key (fast)
- Encrypt symmetric key with recipient's public key
- Send both encrypted data and encrypted key
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def hybrid_encrypt(plaintext, recipient_public_key):
"""
Hybrid encryption: Use RSA to encrypt AES key, use AES to encrypt data
"""
# 1. Generate random AES key
aes_key = AESGCM.generate_key(bit_length=256)
# 2. Encrypt plaintext with AES-GCM
aesgcm = AESGCM(aes_key)
nonce = os.urandom(12)
if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# 3. Encrypt AES key with recipient's RSA public key
encrypted_key = recipient_public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return {
'encrypted_key': encrypted_key,
'nonce': nonce,
'ciphertext': ciphertext
}
def hybrid_decrypt(encrypted_data, private_key):
"""
Hybrid decryption
"""
# 1. Decrypt AES key with RSA private key
aes_key = private_key.decrypt(
encrypted_data['encrypted_key'],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# 2. Decrypt ciphertext with AES key
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(
encrypted_data['nonce'],
encrypted_data['ciphertext'],
None
)
return plaintext.decode('utf-8')
# Example
rsa_crypto = RSAEncryption()
large_message = "This is a large message that would be inefficient to encrypt with RSA alone. " * 100
encrypted = hybrid_encrypt(large_message, rsa_crypto.public_key)
decrypted = hybrid_decrypt(encrypted, rsa_crypto.private_key)
print(f"Original length: {len(large_message)}")
print(f"Decryption successful: {decrypted == large_message}")
Digital Signatures
Digital signatures provide authentication (proves who created it) and non-repudiation (creator cannot deny creating it).
How Digital Signatures Work
- Signing: Hash the message, then encrypt hash with private key
- Verification: Decrypt signature with public key, compare with hash of message
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
class DigitalSignature:
def __init__(self):
# Generate key pair
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
self.public_key = self.private_key.public_key()
def sign(self, message):
"""Create digital signature of message"""
if isinstance(message, str):
message = message.encode('utf-8')
signature = self.private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return signature
def verify(self, message, signature):
"""Verify digital signature"""
if isinstance(message, str):
message = message.encode('utf-8')
try:
self.public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception as e:
return False
# Example usage
signer = DigitalSignature()
# Alice signs a message
message = "I agree to the terms of this contract"
signature = signer.sign(message)
print(f"Signature: {signature.hex()[:64]}...")
# Bob verifies the signature
is_valid = signer.verify(message, signature)
print(f"Signature valid: {is_valid}")
# Tampering detection
tampered_message = "I do not agree to the terms of this contract"
is_valid = signer.verify(tampered_message, signature)
print(f"Tampered message valid: {is_valid}") # False
Use Case: Software Updates
import json
import base64
class SoftwareUpdateSigner:
def __init__(self, private_key):
self.private_key = private_key
def create_signed_update(self, version, download_url, checksum):
"""Create signed update manifest"""
update_info = {
'version': version,
'download_url': download_url,
'checksum': checksum,
'timestamp': '2025-12-23T10:00:00Z'
}
# Create canonical JSON (sorted keys for consistency)
canonical = json.dumps(update_info, sort_keys=True)
# Sign the update info
signature = self.private_key.sign(
canonical.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Return manifest with signature
return {
'update': update_info,
'signature': base64.b64encode(signature).decode('utf-8')
}
class SoftwareUpdateVerifier:
def __init__(self, public_key):
self.public_key = public_key
def verify_update(self, manifest):
"""Verify signed update manifest"""
try:
# Extract signature
signature = base64.b64decode(manifest['signature'])
# Recreate canonical JSON
canonical = json.dumps(manifest['update'], sort_keys=True)
# Verify signature
self.public_key.verify(
signature,
canonical.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception as e:
print(f"Verification failed: {e}")
return False
# Example: Software vendor publishes update
vendor_keys = RSAEncryption()
signer = SoftwareUpdateSigner(vendor_keys.private_key)
update_manifest = signer.create_signed_update(
version="2.0.1",
download_url="https://example.com/app-2.0.1.zip",
checksum="abc123def456..."
)
print("Update manifest:")
print(json.dumps(update_manifest, indent=2))
# Client verifies update
verifier = SoftwareUpdateVerifier(vendor_keys.public_key)
is_valid = verifier.verify_update(update_manifest)
print(f"\nUpdate verification: {is_valid}")
Key Management
The security of cryptographic systems depends entirely on keeping keys secret. Key management is often the weakest link.
Best Practices
import os
from pathlib import Path
# BAD: Hardcoded keys
API_KEY = "sk_live_abc123" # NEVER DO THIS!
# BAD: Keys in version control
# config.py:
# SECRET_KEY = "my-secret-key"
# GOOD: Environment variables
import os
api_key = os.environ.get('API_KEY')
if not api_key:
raise ValueError("API_KEY environment variable not set")
# GOOD: Key files with proper permissions
def load_key_secure(key_file_path):
"""Load key from file with permission check"""
path = Path(key_file_path)
# Check file permissions (Unix/Linux)
stat = path.stat()
mode = oct(stat.st_mode)[-3:]
if mode != '600': # Should be readable/writable by owner only
raise PermissionError(f"Key file has insecure permissions: {mode}")
with open(key_file_path, 'rb') as f:
return f.read()
# GOOD: Key rotation
class KeyManager:
def __init__(self):
self.keys = {}
self.active_key_id = None
def add_key(self, key_id, key):
"""Add a new key"""
self.keys[key_id] = {
'key': key,
'created': datetime.now()
}
def set_active_key(self, key_id):
"""Set which key to use for new encryption"""
self.active_key_id = key_id
def encrypt(self, plaintext):
"""Encrypt with active key"""
key_id = self.active_key_id
key = self.keys[key_id]['key']
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
# Include key ID so we know which key to use for decryption
return {
'key_id': key_id,
'nonce': nonce,
'ciphertext': ciphertext
}
def decrypt(self, encrypted_data):
"""Decrypt with appropriate key"""
key_id = encrypted_data['key_id']
key = self.keys[key_id]['key']
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(
encrypted_data['nonce'],
encrypted_data['ciphertext'],
None
)
return plaintext.decode('utf-8')
# Example: Key rotation
manager = KeyManager()
# Add initial key
manager.add_key('key-2024-01', AESGCM.generate_key(bit_length=256))
manager.set_active_key('key-2024-01')
# Encrypt some data
data1 = manager.encrypt("sensitive data 1")
# Later: Rotate to new key
manager.add_key('key-2024-02', AESGCM.generate_key(bit_length=256))
manager.set_active_key('key-2024-02')
# New encryptions use new key
data2 = manager.encrypt("sensitive data 2")
# But we can still decrypt old data
print(manager.decrypt(data1)) # Uses key-2024-01
print(manager.decrypt(data2)) # Uses key-2024-02
Common Cryptographic Mistakes
1. Rolling Your Own Crypto
# NEVER DO THIS
def my_encryption(plaintext, key):
"""My custom super secure encryption algorithm"""
result = ""
for i, char in enumerate(plaintext):
result += chr(ord(char) ^ ord(key[i % len(key)]))
return result
# INSTEAD: Use well-tested libraries
from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
encrypted = f.encrypt(b"my secret")
2. Using Weak Random Number Generators
import random
# BAD: Not cryptographically secure
random.seed(12345)
weak_key = ''.join(random.choices('0123456789abcdef', k=32))
# GOOD: Use cryptographically secure RNG
import secrets
strong_key = secrets.token_hex(32)
3. Not Using Authenticated Encryption
# BAD: Encryption without authentication (vulnerable to tampering)
# Using plain AES-CBC or AES-CTR without HMAC
# GOOD: Use AEAD modes like AES-GCM
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# GCM provides both encryption and authentication
4. Improper IV/Nonce Usage
# BAD: Reusing IV
iv = b'0' * 16
cipher1 = Cipher(algorithms.AES(key), modes.CBC(iv))
cipher2 = Cipher(algorithms.AES(key), modes.CBC(iv)) # Same IV!
# GOOD: Generate random IV for each encryption
import os
iv = os.urandom(16)
Exercises
Basic Exercises
Hash Function Properties: Explain why each property is important:
- Deterministic
- One-way (irreversible)
- Collision-resistant
- Avalanche effect
Password Storage: Fix this insecure password storage code:
def store_password(username, password):
hashed = hashlib.md5(password.encode()).hexdigest()
db.execute("INSERT INTO users VALUES (?, ?)", (username, hashed))
- Symmetric vs Asymmetric: For each scenario, would you use symmetric or asymmetric encryption?
- Encrypting a 1GB database backup
- Sending a message to someone you've never met
- Encrypting your laptop's hard drive
- Establishing a secure connection to a website
Intermediate Exercises
File Encryption Tool: Create a command-line tool that:
- Encrypts a file with AES-256-GCM
- Stores the encrypted file with nonce and authentication tag
- Can decrypt the file back
- Uses a password-derived key (PBKDF2)
Message Verification: Implement a system where:
- Alice can send a signed message to Bob
- Bob can verify the message came from Alice and wasn't tampered with
- Use RSA for signatures and include timestamp to prevent replay attacks
Secure API Request Signing: Create an HMAC-based request signing system:
- Client signs requests with shared secret
- Server verifies signature before processing
- Include timestamp to prevent replay attacks
- Sign both URL and request body
Advanced Exercises
Secure File Sharing: Design and implement a secure file sharing system:
- User A can upload encrypted file
- User A can grant access to User B without re-encrypting
- Use hybrid encryption (RSA + AES)
- Include integrity verification
- Support key rotation
Certificate Chain Verification: Implement basic X.509 certificate chain verification:
- Load a certificate chain
- Verify each certificate is signed by the next in chain
- Check certificate validity periods
- Verify the root certificate is trusted
Secure Communication Protocol: Design a secure messaging protocol:
- Initial key exchange using RSA
- Ongoing communication using AES
- Perfect forward secrecy (new keys for each session)
- Message authentication and ordering
- Replay attack prevention
Summary
Cryptography provides the mathematical tools for secure communication and data protection:
Hashing: One-way functions that create fixed-size fingerprints of data. Essential for password storage (with proper salting and key derivation), integrity verification, and message authentication.
Symmetric Encryption: Fast encryption using the same key for encryption and decryption. AES-GCM is the recommended algorithm, providing both confidentiality and authenticity.
Asymmetric Encryption: Uses public/private key pairs. Solves the key distribution problem but is much slower than symmetric encryption. RSA is widely used, typically in hybrid schemes.
Digital Signatures: Provide authentication and non-repudiation. Essential for software updates, certificates, and any scenario requiring proof of origin.
Key Management: The weakest link in most systems. Never hardcode keys, use environment variables or key management services, rotate keys regularly, and use proper access controls.
Critical Principles
- Never roll your own cryptography
- Use well-tested libraries and standard algorithms
- Use authenticated encryption (AEAD modes like GCM)
- Generate cryptographically secure random values
- Never reuse IVs/nonces with the same key
- Use appropriate key sizes (AES-256, RSA-2048 minimum)
- Implement proper key management and rotation
- Hash passwords with salt and key derivation functions (bcrypt, Argon2)
Key Takeaways
- Cryptography is a tool, not a complete security solution
- Modern crypto libraries make it easier to use cryptography correctly
- Most vulnerabilities come from improper use, not weak algorithms
- Key management is often harder than the cryptography itself
- When in doubt, use authenticated encryption (AES-GCM)
- Asymmetric crypto is for key exchange, symmetric crypto is for data encryption
Further Reading
- "Serious Cryptography" by Jean-Philippe Aumasson
- "Cryptography Engineering" by Ferguson, Schneier, and Kohno
- NIST Cryptographic Standards: https://csrc.nist.gov/
- cryptography.io documentation: https://cryptography.io/
- OWASP Cryptographic Storage Cheat Sheet
Next Steps
Continue to 03-auth.md to learn how cryptography is applied to authentication and authorization systems, including passwords, sessions, tokens, and OAuth.