🎯 Learning Objectives
- Understand the fundamental goals of cryptography: confidentiality, integrity, authentication, and non-repudiation
- Learn the historical evolution of cryptography from classical to modern systems
- Master the distinction between symmetric and asymmetric cryptography
- Understand cryptographic hashing, digital signatures, and message authentication codes
- Implement practical cryptographic solutions using Python
- Recognize common cryptographic attacks and security best practices
- Apply proper cryptographic protocols in real-world scenarios
⏱ Timing Breakdown
| Segment | Time |
|---|---|
| 0. Introduction & Historical Context | 15 min |
| 1. Cryptographic Goals & Adversarial Models | 10 min |
| 2. Classical Cryptography & Historical Ciphers | 20 min |
| 3. Cryptographic Hashing & Message Digests | 20 min |
| 4. Symmetric Cryptography | 25 min |
| 5. Asymmetric Cryptography & Digital Signatures | 25 min |
| 6. Cryptanalysis & Attack Methods | 15 min |
| 7. Modern Protocols & Hybrid Systems | 15 min |
| 8. Security Best Practices | 15 min |
| 9. Practical Exercises & Wrap-Up | 10 min |
0. Introduction & Historical Context (15 min)
What is Cryptography?
Cryptography is the art and science of making ciphers - creating secure communication systems that protect information from unauthorized access.
Cryptanalysis is the art and science of breaking ciphers - analyzing and defeating cryptographic systems.
Cryptology encompasses both cryptography and cryptanalysis.
Historical Timeline
- Ancient Times: Caesar cipher, substitution ciphers
- World War I & II: Enigma machine, codebreaking efforts
- 1970s: DES (Data Encryption Standard), public key cryptography
- 1990s: RSA, AES development
- 2000s: Elliptic curve cryptography, SHA-3
- Present: Post-quantum cryptography, quantum-resistant algorithms
Key Historical Figures
- Julius Caesar: Caesar cipher
- Alan Turing: Enigma codebreaking, computational theory
- Whitfield Diffie & Martin Hellman: Public key cryptography
- Ron Rivest, Adi Shamir, Leonard Adleman: RSA algorithm
1. Cryptographic Goals & Adversarial Models (10 min)
The Four Pillars of Cryptography
1. Confidentiality
- Keep information secret from unauthorized parties
- Only intended recipients can read the message
- Example: Encrypted email, secure messaging
2. Integrity
- Detect if information has been tampered with
- Ensure data hasn't been modified in transit
- Example: File checksums, digital signatures
3. Authentication
- Verify the identity of communicating parties
- Confirm who sent the message
- Example: Login systems, digital certificates
4. Non-repudiation
- Prevent sender from denying they sent a message
- Provide proof of message origin
- Example: Digital signatures, audit trails
Adversarial Models
Eve (Eavesdropper)
- Can observe communication but cannot modify it
- Passive attacker
- Threat: Confidentiality
Mallory (Malicious Actor)
- Can intercept, modify, inject, or delete messages
- Active attacker
- Threats: All four cryptographic goals
Kerckhoffs' Principle
"A cryptosystem should be secure even if everything about the system, except the key, is public knowledge."
Why this matters: - Algorithm secrecy is fragile (people leave, algorithms leak) - Public algorithms undergo extensive security analysis - Key management is easier than algorithm secrecy
2. Classical Cryptography & Historical Ciphers (20 min)
Types of Classical Ciphers
Substitution Ciphers
Replace each character with another character.
Caesar Cipher
def caesar_cipher(text, shift):
result = ""
for char in text:
if char.isalpha():
ascii_offset = 65 if char.isupper() else 97
shifted = (ord(char) - ascii_offset + shift) % 26
result += chr(shifted + ascii_offset)
else:
result += char
return result
# Example
plaintext = "ATTACKATDAWN"
ciphertext = caesar_cipher(plaintext, 5)
print(f"Plaintext: {plaintext}")
print(f"Ciphertext: {ciphertext}")
Monoalphabetic Substitution
import random
import string
def monoalphabetic_substitution(text, key=None):
if key is None:
alphabet = list(string.ascii_uppercase)
random.shuffle(alphabet)
key = ''.join(alphabet)
result = ""
for char in text.upper():
if char.isalpha():
index = ord(char) - ord('A')
result += key[index]
else:
result += char
return result
# Example
plaintext = "ATTACKATDAWN"
ciphertext = monoalphabetic_substitution(plaintext)
print(f"Plaintext: {plaintext}")
print(f"Ciphertext: {ciphertext}")
Transposition Ciphers
Rearrange characters without changing them.
Rail Fence Cipher
def rail_fence_encrypt(text, rails):
fence = [[] for _ in range(rails)]
rail = 0
direction = 1
for char in text:
fence[rail].append(char)
rail += direction
if rail == rails - 1 or rail == 0:
direction = -direction
return ''.join([''.join(rail) for rail in fence])
# Example
plaintext = "WEAREDISCOVEREDFLEEATONCE"
ciphertext = rail_fence_encrypt(plaintext, 3)
print(f"Plaintext: {plaintext}")
print(f"Ciphertext: {ciphertext}")
Breaking Classical Ciphers
Frequency Analysis
from collections import Counter
def frequency_analysis(text):
# Remove non-letters and convert to uppercase
clean_text = ''.join(c.upper() for c in text if c.isalpha())
# Count letter frequencies
frequencies = Counter(clean_text)
total_letters = len(clean_text)
# Calculate percentages
percentages = {letter: (count/total_letters)*100
for letter, count in frequencies.items()}
return sorted(percentages.items(), key=lambda x: x[1], reverse=True)
# English letter frequency (approximate)
english_freq = {
'E': 12.7, 'T': 9.1, 'A': 8.2, 'O': 7.5, 'I': 7.0, 'N': 6.7,
'S': 6.3, 'H': 6.1, 'R': 6.0, 'D': 4.3, 'L': 4.0, 'C': 2.8,
'U': 2.8, 'M': 2.4, 'W': 2.4, 'F': 2.2, 'G': 2.0, 'Y': 2.0,
'P': 1.9, 'B': 1.3, 'V': 1.0, 'K': 0.8, 'J': 0.15, 'X': 0.15,
'Q': 0.10, 'Z': 0.07
}
# Example usage
sample_text = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"
freq_analysis = frequency_analysis(sample_text)
print("Letter frequencies:")
for letter, freq in freq_analysis:
print(f"{letter}: {freq:.2f}%")
3. Cryptographic Hashing & Message Digests (20 min)
Hash Function Properties
A cryptographic hash function H produces a hash from a message: H(m) = c
Essential Properties:
- Pre-image Resistance: Given hash c, computationally infeasible to find m such that H(m) = c
- Second Pre-image Resistance: Given m₁, computationally infeasible to find m₂ ≠ m₁ such that H(m₁) = H(m₂)
- Collision Resistance: Computationally infeasible to find any m₁ ≠ m₂ such that H(m₁) = H(m₂)
Common Hash Algorithms
SHA Family
import hashlib
def demonstrate_hash_functions():
message = b"Hello, World!"
# SHA-1 (deprecated - collision attacks possible)
sha1_hash = hashlib.sha1(message).hexdigest()
print(f"SHA-1: {sha1_hash}")
# SHA-256 (widely used)
sha256_hash = hashlib.sha256(message).hexdigest()
print(f"SHA-256: {sha256_hash}")
# SHA-3 (newest standard)
sha3_hash = hashlib.sha3_256(message).hexdigest()
print(f"SHA-3: {sha3_hash}")
# Demonstrate avalanche effect
message2 = b"Hello, World?"
sha256_hash2 = hashlib.sha256(message2).hexdigest()
print(f"SHA-256 (changed): {sha256_hash2}")
demonstrate_hash_functions()
Password Hashing with Salt
Secure Password Storage
import os
import hashlib
import secrets
def hash_password(password: str, salt: bytes = None) -> tuple:
"""
Hash a password with a random salt using PBKDF2.
Returns (salt, hash) tuple.
"""
if salt is None:
salt = os.urandom(32) # 256-bit salt
# Use PBKDF2 with SHA-256
hash_obj = hashlib.pbkdf2_hmac('sha256',
password.encode('utf-8'),
salt,
100000) # 100,000 iterations
return salt, hash_obj
def verify_password(password: str, salt: bytes, stored_hash: bytes) -> bool:
"""Verify a password against its stored hash."""
_, computed_hash = hash_password(password, salt)
return secrets.compare_digest(computed_hash, stored_hash)
# Example usage
password = "correct horse battery staple"
salt, password_hash = hash_password(password)
print(f"Salt: {salt.hex()}")
print(f"Hash: {password_hash.hex()}")
# Verify password
is_valid = verify_password(password, salt, password_hash)
print(f"Password verification: {is_valid}")
# Test with wrong password
is_invalid = verify_password("wrong password", salt, password_hash)
print(f"Wrong password verification: {is_invalid}")
Message Authentication Codes (MACs)
HMAC Implementation
import hmac
import hashlib
def create_hmac(message: bytes, key: bytes) -> bytes:
"""Create HMAC using SHA-256."""
return hmac.new(key, message, hashlib.sha256).digest()
def verify_hmac(message: bytes, key: bytes, received_mac: bytes) -> bool:
"""Verify HMAC."""
expected_mac = create_hmac(message, key)
return hmac.compare_digest(expected_mac, received_mac)
# Example
message = b"Important message"
key = os.urandom(32) # 256-bit key
mac = create_hmac(message, key)
print(f"HMAC: {mac.hex()}")
# Verify
is_valid = verify_hmac(message, key, mac)
print(f"HMAC verification: {is_valid}")
4. Symmetric Cryptography (25 min)
Symmetric vs Asymmetric
Symmetric Cryptography: - Same key for encryption and decryption - Fast and efficient - Key distribution problem - Examples: AES, ChaCha20, DES
Asymmetric Cryptography: - Different keys for encryption and decryption - Slower but solves key distribution - Examples: RSA, ECC, Diffie-Hellman
Block Ciphers vs Stream Ciphers
Block Ciphers
- Process fixed-size blocks (e.g., 128 bits)
- Examples: AES, DES, Blowfish
- Modes: ECB, CBC, CFB, OFB, GCM
Stream Ciphers
- Process data bit-by-bit or byte-by-byte
- Examples: ChaCha20, RC4, A5/1
AES (Advanced Encryption Standard)
AES-GCM Implementation
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
class AESCipher:
def __init__(self, key_size=32): # 256-bit key
self.key = get_random_bytes(key_size)
def encrypt(self, plaintext: bytes) -> tuple:
"""Encrypt using AES-GCM mode."""
cipher = AES.new(self.key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return cipher.nonce, ciphertext, tag
def decrypt(self, nonce: bytes, ciphertext: bytes, tag: bytes) -> bytes:
"""Decrypt using AES-GCM mode."""
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag)
# Example usage
aes = AESCipher()
message = b"This is a secret message"
# Encrypt
nonce, ciphertext, tag = aes.encrypt(message)
print(f"Nonce: {nonce.hex()}")
print(f"Ciphertext: {ciphertext.hex()}")
print(f"Tag: {tag.hex()}")
# Decrypt
decrypted = aes.decrypt(nonce, ciphertext, tag)
print(f"Decrypted: {decrypted.decode()}")
AES-CBC Implementation
def aes_cbc_encrypt(plaintext: bytes, key: bytes) -> tuple:
"""Encrypt using AES-CBC mode."""
iv = get_random_bytes(16) # 128-bit IV
cipher = AES.new(key, AES.MODE_CBC, iv)
# Pad the plaintext
padded_plaintext = pad(plaintext, AES.block_size)
ciphertext = cipher.encrypt(padded_plaintext)
return iv, ciphertext
def aes_cbc_decrypt(iv: bytes, ciphertext: bytes, key: bytes) -> bytes:
"""Decrypt using AES-CBC mode."""
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_plaintext = cipher.decrypt(ciphertext)
return unpad(padded_plaintext, AES.block_size)
# Example
key = get_random_bytes(32) # 256-bit key
message = b"Hello, World! This is a longer message."
# Encrypt
iv, ciphertext = aes_cbc_encrypt(message, key)
print(f"IV: {iv.hex()}")
print(f"Ciphertext: {ciphertext.hex()}")
# Decrypt
decrypted = aes_cbc_decrypt(iv, ciphertext, key)
print(f"Decrypted: {decrypted.decode()}")
Modes of Operation
ECB (Electronic Codebook) - INSECURE
def aes_ecb_encrypt(plaintext: bytes, key: bytes) -> bytes:
"""ECB mode - DO NOT USE for real applications!"""
cipher = AES.new(key, AES.MODE_ECB)
padded_plaintext = pad(plaintext, AES.block_size)
return cipher.encrypt(padded_plaintext)
# ECB reveals patterns - demonstrate with image-like data
def demonstrate_ecb_weakness():
# Create a pattern (like a simple image)
pattern = b"AAAA" * 4 + b"BBBB" * 4 # 32 bytes total
key = get_random_bytes(32)
ecb_ciphertext = aes_ecb_encrypt(pattern, key)
print(f"ECB ciphertext: {ecb_ciphertext.hex()}")
print("Notice the repeating pattern!")
demonstrate_ecb_weakness()
5. Asymmetric Cryptography & Digital Signatures (25 min)
Public Key Cryptography Concepts
Key Pairs
- Public Key: Can be shared with everyone
- Private Key: Must be kept secret
- Mathematical Relationship: Keys are mathematically related but computationally infeasible to derive one from the other
RSA Implementation
RSA Key Generation
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
def generate_rsa_keypair(key_size=2048):
"""Generate RSA key pair."""
key = RSA.generate(key_size)
private_key = key.export_key()
public_key = key.publickey().export_key()
return private_key, public_key
def rsa_encrypt(message: bytes, public_key_bytes: bytes) -> bytes:
"""Encrypt message using RSA public key."""
public_key = RSA.import_key(public_key_bytes)
cipher = PKCS1_OAEP.new(public_key)
# base64 encode
return cipher.encrypt(message)
def rsa_decrypt(ciphertext: bytes, private_key_bytes: bytes) -> bytes:
"""Decrypt message using RSA private key."""
private_key = RSA.import_key(private_key_bytes)
# base64 decode
cipher = PKCS1_OAEP.new(private_key)
return cipher.decrypt(ciphertext)
# Example
private_key, public_key = generate_rsa_keypair()
message = b"Secret message for RSA encryption"
# Encrypt
ciphertext = rsa_encrypt(message, public_key)
print(f"Ciphertext: {ciphertext.hex()}")
# Decrypt
decrypted = rsa_decrypt(ciphertext, private_key)
print(f"Decrypted: {decrypted.decode()}")
RSA Digital Signatures
def rsa_sign(message: bytes, private_key_bytes: bytes) -> bytes:
"""Sign message using RSA private key."""
private_key = RSA.import_key(private_key_bytes)
hash_obj = SHA256.new(message)
signature = pkcs1_15.new(private_key).sign(hash_obj)
return signature
def rsa_verify(message: bytes, signature: bytes, public_key_bytes: bytes) -> bool:
"""Verify RSA signature."""
public_key = RSA.import_key(public_key_bytes)
hash_obj = SHA256.new(message)
try:
pkcs1_15.new(public_key).verify(hash_obj, signature)
return True
except ValueError:
return False
# Example
message = b"Important document to sign"
signature = rsa_sign(message, private_key)
# Verify signature
is_valid = rsa_verify(message, signature, public_key)
print(f"Signature valid: {is_valid}")
# Test with tampered message
tampered_message = b"Important document to sign (modified)"
is_invalid = rsa_verify(tampered_message, signature, public_key)
print(f"Tampered message signature valid: {is_invalid}")
Elliptic Curve Cryptography (ECC)
ECDSA Implementation
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature, decode_dss_signature
def generate_ec_keypair():
"""Generate ECDSA key pair."""
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
return private_key, public_key
def ecdsa_sign(message: bytes, private_key) -> bytes:
"""Sign message using ECDSA."""
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
return signature
def ecdsa_verify(message: bytes, signature: bytes, public_key) -> bool:
"""Verify ECDSA signature."""
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
return True
except Exception:
return False
# Example
private_key, public_key = generate_ec_keypair()
message = b"ECDSA signed message"
# Sign
signature = ecdsa_sign(message, private_key)
print(f"ECDSA signature: {signature.hex()}")
# Verify
is_valid = ecdsa_verify(message, signature, public_key)
print(f"ECDSA signature valid: {is_valid}")
Diffie-Hellman Key Exchange
DH Implementation
from cryptography.hazmat.primitives.asymmetric import dh
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
def generate_dh_parameters():
"""Generate DH parameters."""
parameters = dh.generate_parameters(generator=2, key_size=2048)
return parameters
def perform_dh_key_exchange():
"""Demonstrate DH key exchange."""
# Generate parameters
parameters = generate_dh_parameters()
# Alice generates her key pair
alice_private = parameters.generate_private_key()
alice_public = alice_private.public_key()
# Bob generates his key pair
bob_private = parameters.generate_private_key()
bob_public = bob_private.public_key()
# Exchange public keys and compute shared secret
alice_shared = alice_private.exchange(bob_public)
bob_shared = bob_private.exchange(alice_public)
# Derive symmetric key from shared secret
derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b'handshake data',
).derive(alice_shared)
print(f"Shared secret length: {len(alice_shared)} bytes")
print(f"Derived key: {derived_key.hex()}")
print(f"Keys match: {alice_shared == bob_shared}")
perform_dh_key_exchange()
6. Cryptanalysis & Attack Methods (15 min)
Types of Attacks
Ciphertext-Only Attack
- Attacker has only ciphertext
- Example: Frequency analysis on substitution ciphers
Known Plaintext Attack
- Attacker has some plaintext-ciphertext pairs
- Example: Breaking Enigma with known message formats
Chosen Plaintext Attack
- Attacker can choose plaintexts to encrypt
- Example: Differential cryptanalysis
Chosen Ciphertext Attack
- Attacker can choose ciphertexts to decrypt
- Example: Padding oracle attacks
Common Attack Techniques
Frequency Analysis Attack
def frequency_attack_caesar(ciphertext: str):
"""Attempt to break Caesar cipher using frequency analysis."""
# English letter frequencies
english_freq = {
'E': 12.7, 'T': 9.1, 'A': 8.2, 'O': 7.5, 'I': 7.0, 'N': 6.7,
'S': 6.3, 'H': 6.1, 'R': 6.0, 'D': 4.3, 'L': 4.0, 'C': 2.8,
'U': 2.8, 'M': 2.4, 'W': 2.4, 'F': 2.2, 'G': 2.0, 'Y': 2.0,
'P': 1.9, 'B': 1.3, 'V': 1.0, 'K': 0.8, 'J': 0.15, 'X': 0.15,
'Q': 0.10, 'Z': 0.07
}
best_shift = 0
best_score = float('inf')
for shift in range(26):
# Decrypt with current shift
decrypted = caesar_cipher(ciphertext, -shift)
# Calculate frequency score
freq_analysis = frequency_analysis(decrypted)
score = 0
for letter, freq in freq_analysis:
expected_freq = english_freq.get(letter, 0)
score += abs(freq - expected_freq)
if score < best_score:
best_score = score
best_shift = shift
return caesar_cipher(ciphertext, -best_shift), best_shift
# Example
original_message = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"
encrypted_message = caesar_cipher(original_message, 7)
print(f"Original: {original_message}")
print(f"Encrypted: {encrypted_message}")
decrypted_message, shift_used = frequency_attack_caesar(encrypted_message)
print(f"Decrypted: {decrypted_message}")
print(f"Shift used: {shift_used}")
Padding Oracle Attack Simulation
def simulate_padding_oracle_attack():
"""
Simulate a padding oracle attack on CBC mode.
This is a simplified demonstration.
"""
print("Padding Oracle Attack Simulation")
print("=" * 40)
# Generate random key and IV
key = get_random_bytes(16)
iv = get_random_bytes(16)
# Encrypt a message
message = b"Hello, World!"
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_message = pad(message, AES.block_size)
ciphertext = cipher.encrypt(padded_message)
print(f"Original message: {message}")
print(f"Ciphertext: {ciphertext.hex()}")
# Simulate padding oracle (checks if padding is valid)
def padding_oracle(ciphertext_bytes):
try:
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext_bytes)
unpad(decrypted, AES.block_size)
return True # Valid padding
except ValueError:
return False # Invalid padding
# Attack: modify last byte of ciphertext and test padding oracle
modified_ciphertext = bytearray(ciphertext)
for i in range(256):
modified_ciphertext[-1] = i
if padding_oracle(bytes(modified_ciphertext)):
print(f"Found valid padding with byte value: {i}")
break
simulate_padding_oracle_attack()
7. Modern Protocols & Hybrid Systems (15 min)
Hybrid Cryptography
Why Hybrid Systems? - Symmetric encryption is fast but has key distribution problems - Asymmetric encryption solves key distribution but is slow - Solution: Use asymmetric for key exchange, symmetric for bulk data
TLS/SSL Handshake Simulation
def simulate_tls_handshake():
"""Simulate a simplified TLS handshake."""
print("TLS Handshake Simulation")
print("=" * 30)
# 1. Client generates random pre-master secret
pre_master_secret = get_random_bytes(48)
print(f"1. Client generates pre-master secret: {pre_master_secret.hex()[:16]}...")
# 2. Server generates RSA key pair
server_private, server_public = generate_rsa_keypair()
print("2. Server generates RSA key pair")
# 3. Client encrypts pre-master secret with server's public key
encrypted_pre_master = rsa_encrypt(pre_master_secret, server_public)
print(f"3. Client encrypts pre-master secret: {encrypted_pre_master.hex()[:16]}...")
# 4. Server decrypts pre-master secret
decrypted_pre_master = rsa_decrypt(encrypted_pre_master, server_private)
print(f"4. Server decrypts pre-master secret: {decrypted_pre_master.hex()[:16]}...")
# 5. Both parties derive session keys
# (In real TLS, this involves more complex key derivation)
session_key = hashlib.sha256(pre_master_secret).digest()[:32]
print(f"5. Session key derived: {session_key.hex()[:16]}...")
# 6. Use session key for symmetric encryption
aes_cipher = AESCipher()
aes_cipher.key = session_key # Override with derived key
message = b"Secure message using session key"
nonce, ciphertext, tag = aes_cipher.encrypt(message)
print(f"6. Message encrypted with session key: {ciphertext.hex()[:16]}...")
decrypted_message = aes_cipher.decrypt(nonce, ciphertext, tag)
print(f"7. Message decrypted: {decrypted_message.decode()}")
simulate_tls_handshake()
Digital Certificates & PKI
Certificate Verification Simulation
def simulate_certificate_verification():
"""Simulate certificate verification process."""
print("Certificate Verification Simulation")
print("=" * 40)
# 1. CA generates root certificate
ca_private, ca_public = generate_rsa_keypair()
print("1. CA generates root certificate")
# 2. Server generates certificate request
server_private, server_public = generate_rsa_keypair()
print("2. Server generates certificate request")
# 3. CA signs server's public key
server_cert_data = server_public # Simplified
server_cert_signature = rsa_sign(server_cert_data, ca_private)
print("3. CA signs server certificate")
# 4. Client verifies certificate
is_valid = rsa_verify(server_cert_data, server_cert_signature, ca_public)
print(f"4. Client verifies certificate: {is_valid}")
if is_valid:
print("5. Certificate is valid - proceeding with secure communication")
else:
print("5. Certificate verification failed - aborting connection")
simulate_certificate_verification()
8. Security Best Practices (15 min)
⚠️ Critical Security Rules
1. NEVER Roll Your Own Crypto
# ❌ WRONG - Don't do this!
def bad_encryption(plaintext, key):
result = ""
for i, char in enumerate(plaintext):
result += chr(ord(char) ^ ord(key[i % len(key)]))
return result
# ✅ CORRECT - Use established libraries
from cryptography.fernet import Fernet
def good_encryption(plaintext, key):
f = Fernet(key)
return f.encrypt(plaintext)
2. Use Appropriate Key Sizes
def demonstrate_key_sizes():
"""Demonstrate appropriate key sizes for different algorithms."""
print("Recommended Key Sizes:")
print("=" * 25)
print("AES: 128-bit (minimum), 256-bit (recommended)")
print("RSA: 2048-bit (minimum), 3072-bit (recommended)")
print("ECC: 256-bit (equivalent to 3072-bit RSA)")
print("Hash: SHA-256 (minimum), SHA-3 (recommended)")
# Generate keys with appropriate sizes
aes_key = get_random_bytes(32) # 256-bit
rsa_key = RSA.generate(2048) # 2048-bit
print(f"AES key length: {len(aes_key) * 8} bits")
print(f"RSA key length: {rsa_key.size_in_bits()} bits")
demonstrate_key_sizes()
3. Secure Random Number Generation
import secrets
def demonstrate_secure_randomness():
"""Demonstrate secure vs insecure random number generation."""
print("Random Number Generation:")
print("=" * 30)
# ❌ Insecure - predictable
import random
random.seed(42) # Fixed seed
insecure_random = random.randint(0, 1000000)
print(f"Insecure random: {insecure_random}")
# ✅ Secure - cryptographically secure
secure_random = secrets.randbelow(1000000)
print(f"Secure random: {secure_random}")
# Generate secure random bytes
secure_bytes = secrets.token_bytes(32)
print(f"Secure random bytes: {secure_bytes.hex()}")
demonstrate_secure_randomness()
4. Proper Initialization Vectors (IVs)
def demonstrate_iv_usage():
"""Demonstrate proper IV usage."""
print("Initialization Vector Usage:")
print("=" * 30)
key = get_random_bytes(32)
message1 = b"First message"
message2 = b"Second message"
# ❌ WRONG - Reusing IV
iv = get_random_bytes(16)
cipher1 = AES.new(key, AES.MODE_CBC, iv)
cipher2 = AES.new(key, AES.MODE_CBC, iv) # Same IV!
ct1 = cipher1.encrypt(pad(message1, AES.block_size))
ct2 = cipher2.encrypt(pad(message2, AES.block_size))
print("❌ Reusing IV reveals patterns")
print(f"Message 1 ciphertext: {ct1.hex()[:32]}...")
print(f"Message 2 ciphertext: {ct2.hex()[:32]}...")
# ✅ CORRECT - Unique IV for each message
iv1 = get_random_bytes(16)
iv2 = get_random_bytes(16)
cipher1 = AES.new(key, AES.MODE_CBC, iv1)
cipher2 = AES.new(key, AES.MODE_CBC, iv2)
ct1 = cipher1.encrypt(pad(message1, AES.block_size))
ct2 = cipher2.encrypt(pad(message2, AES.block_size))
print("\n✅ Unique IVs provide security")
print(f"Message 1 ciphertext: {ct1.hex()[:32]}...")
print(f"Message 2 ciphertext: {ct2.hex()[:32]}...")
demonstrate_iv_usage()
5. Side-Channel Attack Prevention
import time
def vulnerable_timing_attack():
"""Demonstrate timing attack vulnerability."""
def insecure_compare(a, b):
"""Vulnerable to timing attacks."""
if len(a) != len(b):
return False
for i in range(len(a)):
if a[i] != b[i]:
return False
return True
def secure_compare(a, b):
"""Secure against timing attacks."""
return secrets.compare_digest(a, b)
# Test timing differences
test_string = b"test string"
wrong_string = b"wrong string"
# Time insecure comparison
start = time.time()
insecure_compare(test_string, wrong_string)
insecure_time = time.time() - start
# Time secure comparison
start = time.time()
secure_compare(test_string, wrong_string)
secure_time = time.time() - start
print(f"Insecure compare time: {insecure_time:.6f}s")
print(f"Secure compare time: {secure_time:.6f}s")
vulnerable_timing_attack()
Security Checklist
Before Implementing Crypto:
- [ ] Use established, well-tested libraries
- [ ] Verify algorithm hasn't been broken
- [ ] Use appropriate key sizes
- [ ] Generate random numbers securely
- [ ] Use unique IVs/nonces
- [ ] Implement proper padding
- [ ] Protect against side-channel attacks
- [ ] Validate all inputs
- [ ] Use constant-time comparisons
- [ ] Keep keys secure in memory
9. Practical Exercises & Wrap-Up (10 min)
Hands-On Exercises
Exercise 1: Secure File Encryption
def encrypt_file(filename: str, password: str):
"""Encrypt a file with password-based encryption."""
# Generate salt
salt = os.urandom(32)
# Derive key from password
key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
# Read file
with open(filename, 'rb') as f:
data = f.read()
# Encrypt
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(data)
# Save encrypted file
with open(filename + '.enc', 'wb') as f:
f.write(salt + cipher.nonce + tag + ciphertext)
print(f"File {filename} encrypted to {filename}.enc")
def decrypt_file(filename: str, password: str):
"""Decrypt a password-encrypted file."""
with open(filename, 'rb') as f:
data = f.read()
# Extract components
salt = data[:32]
nonce = data[32:48]
tag = data[48:64]
ciphertext = data[64:]
# Derive key
key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
# Decrypt
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
# Save decrypted file
output_filename = filename.replace('.enc', '.dec')
with open(output_filename, 'wb') as f:
f.write(plaintext)
print(f"File {filename} decrypted to {output_filename}")
# Example usage
# encrypt_file("document.txt", "my_secure_password")
# decrypt_file("document.txt.enc", "my_secure_password")
Exercise 2: Secure Communication Protocol
class SecureMessaging:
def __init__(self):
self.private_key, self.public_key = generate_rsa_keypair()
self.session_key = None
def establish_session(self, peer_public_key):
"""Establish secure session using hybrid encryption."""
# Generate session key
self.session_key = get_random_bytes(32)
# Encrypt session key with peer's public key
encrypted_session_key = rsa_encrypt(self.session_key, peer_public_key)
return encrypted_session_key
def send_message(self, message: bytes, peer_public_key: bytes) -> tuple:
"""Send encrypted and signed message."""
# Encrypt message with session key
cipher = AES.new(self.session_key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(message)
# Sign the message
signature = rsa_sign(message, self.private_key)
return cipher.nonce, ciphertext, tag, signature
def receive_message(self, nonce: bytes, ciphertext: bytes, tag: bytes,
signature: bytes, sender_public_key: bytes) -> bytes:
"""Receive and verify encrypted message."""
# Decrypt message
cipher = AES.new(self.session_key, AES.MODE_GCM, nonce=nonce)
message = cipher.decrypt_and_verify(ciphertext, tag)
# Verify signature
if not rsa_verify(message, signature, sender_public_key):
raise ValueError("Invalid signature")
return message
# Example usage
alice = SecureMessaging()
bob = SecureMessaging()
# Establish session
session_key_encrypted = alice.establish_session(bob.public_key)
bob.session_key = rsa_decrypt(session_key_encrypted, bob.private_key)
# Send message
message = b"Hello Bob, this is Alice!"
nonce, ciphertext, tag, signature = alice.send_message(message, bob.public_key)
# Receive message
received_message = bob.receive_message(nonce, ciphertext, tag, signature, alice.public_key)
print(f"Received message: {received_message.decode()}")
Additional Exercises
- Implement a Caesar cipher breaker using frequency analysis
- Create a password manager with secure storage
- Build a simple TLS-like handshake simulator
- Implement HMAC-based authentication for API requests
- Create a digital signature system for document verification
✅ Summary
Key Takeaways
Cryptographic Goals
- Confidentiality: Keep data secret
- Integrity: Detect tampering
- Authentication: Verify identity
- Non-repudiation: Prevent denial
Algorithm Categories
- Symmetric: Fast, shared key (AES, ChaCha20)
- Asymmetric: Key exchange, signatures (RSA, ECC)
- Hashing: One-way functions (SHA-256, SHA-3)
- MACs: Authentication codes (HMAC)
Security Principles
- Never roll your own crypto
- Use appropriate key sizes
- Generate randomness securely
- Use unique IVs/nonces
- Protect against side-channel attacks
Real-World Applications
- TLS/SSL: Web security
- Digital signatures: Document authentication
- Password hashing: Secure storage
- File encryption: Data protection
- Blockchain: Cryptographic proofs
Further Reading
Remember: Security is a Process, Not a Product
Cryptography is just one tool in the security toolkit. Always consider: - Threat modeling - Defense in depth - Regular security audits - Keeping systems updated - User education and training
"The only secure system is one that is powered off, cast in a block of concrete and sealed in a lead-lined room with armed guards - and even then I have my doubts." - Gene Spafford