Cryptography Fundamentals¶
Table of Contents¶
Foundation Layer¶
- Understanding Cryptography
- Why Crypto Matters
- Classical Ciphers
- Caesar Cipher
- Substitution Ciphers
- Vigenère Cipher
- Transposition Ciphers
- One-Time Pad
- Mathematical Foundations
- Modular Arithmetic
- Prime Numbers
- Euler's Theorem
- Discrete Logarithm
- Chinese Remainder Theorem
- Fermat's Little Theorem
Symmetric Cryptography¶
- Symmetric Encryption Basics
- Stream Ciphers
- RC4 Cipher
- ChaCha20
- Salsa20
- Block Ciphers
- DES and 3DES
- AES Algorithm
- AES Internals
- Block Cipher Modes
- ECB Mode
- CBC Mode
- CTR Mode
- GCM Mode
- Padding Schemes
Asymmetric Cryptography¶
- Public Key Cryptography
- RSA Algorithm
- RSA Key Generation
- RSA Encryption
- RSA Signatures
- Diffie-Hellman
- ECDH
- Elliptic Curves
- Curve25519
- secp256k1
- ECDSA
- EdDSA
- ElGamal
- DSA
- Key Exchange Protocols
Hash Functions and MACs¶
- Hash Functions
- MD5
- SHA Family
- SHA-256
- SHA-3
- BLAKE2
- Message Authentication Codes
- HMAC
- Poly1305
- SipHash
- Authenticated Encryption
- Key Derivation Functions
- PBKDF2
- bcrypt
- scrypt
Advanced Topics¶
- Argon2
- Digital Signatures
- Certificate Authorities
- TLS Protocol
- SSH Protocol
- PGP and GPG
- IPsec
- VPN Protocols
- Kerberos
- OAuth and SAML
- Zero Knowledge Proofs
- Homomorphic Encryption
- Post-Quantum Cryptography
- Lattice-Based Crypto
- Quantum Key Distribution
Understanding Cryptography¶
Crypto keeps secrets in a world where everything leaks
You send data across networks every single day , passwords , credit cards , private messages , all bouncing through routers you don't control and ISPs you don't trust , without cryptography every bit of that data would be readable by anyone sitting in the middle
Think about what happens when you visit a website , your browser connects to your router , router talks to your ISP , ISP routes through backbone providers , eventually hits the destination server , at every hop someone could be watching , logging , modifying
Cryptography solves three problems , confidentiality (only authorized people read it) , integrity (detect if someone changed it) , authenticity (prove who sent it)
Modern crypto isn't optional anymore , it's the foundation of everything digital , HTTPS wouldn't exist without TLS , Bitcoin wouldn't work without ECDSA , your encrypted disk relies on AES , SSH sessions use Diffie-Hellman
The math behind crypto is actually simple once you break it down , modular arithmetic (clock math) , prime numbers (building blocks) , one-way functions (easy forward , impossible backward)
Core Principles:
- Kerckhoffs's Principle: security relies on key secrecy , not algorithm secrecy
- Never roll your own crypto , use tested libraries
- Key management matters more than the algorithm
- Crypto doesn't fix bad system design
Real-World Impact:
Without crypto: passwords transmitted as plaintext , credit cards stolen in transit , governments reading everything , no private communication possible
With crypto: end-to-end encrypted messaging , secure online banking , private browsing , digital signatures proving authenticity
Command to check TLS:
openssl s_client -connect google.com:443 -servername google.com
This initiates a TLS handshake with Google , shows cipher suites negotiated , certificate chain , protocol version , useful for debugging HTTPS issues
Why Crypto Matters¶
Every unencrypted packet is a postcard anyone can read
Picture sending a physical letter , you write it , put it in an envelope , mail it , only the recipient opens it , now imagine sending that same letter as a postcard , everyone who handles it can read every word
That's the internet without encryption , every packet is a postcard
Threat Model:
- Passive attackers: ISPs logging traffic , governments doing mass surveillance , coffee shop WiFi sniffers
- Active attackers: man-in-the-middle attacks , packet injection , DNS hijacking
- Insider threats: compromised routers , malicious sysadmins , supply chain attacks
What Crypto Protects:
- Passwords: hashed with bcrypt/Argon2 so even database dumps don't leak plaintext
- Financial data: credit cards encrypted in transit and at rest
- Private communications: Signal/WhatsApp using end-to-end encryption
- Disk contents: full disk encryption with LUKS/BitLocker
- Code integrity: digital signatures proving software hasn't been tampered with
What Crypto Doesn't Protect:
- Metadata: who talks to who , when , how often (traffic analysis still works)
- Endpoint security: if your device is compromised , crypto can't help
- Social engineering: tricking users into giving up keys
- Implementation bugs: heartbleed , padding oracle , timing attacks
Linux Command to Encrypt Files:
gpg --symmetric --cipher-algo AES256 sensitive_file.txt
This encrypts a file with AES-256 using a passphrase , creates sensitive_file.txt.gpg , decrypt with gpg -d sensitive_file.txt.gpg
Classical Ciphers¶
Ancient crypto that taught us how encryption works
These ciphers are broken , completely insecure , don't use them for anything real , but they're perfect for understanding the fundamentals before diving into AES and RSA
Classical ciphers show up in CTF challenges constantly , understanding them helps you recognize weak crypto in the wild , and honestly they're just fun to play with
Why Learn Broken Crypto:
- Understand substitution vs transposition concepts
- Recognize patterns in ciphertext
- Appreciate how far modern crypto has come
- Solve CTF puzzles that use them as first layers
Caesar Cipher¶
Shift every letter by a fixed number
Julius Caesar used this for military messages over 2000 years ago , it's the simplest cipher that exists , shift A to D , B to E , wrap Z back to C
How it works:
Plaintext: HELLO WORLD
Shift +3: KHOOR ZRUOG
Each letter shifts forward 3 positions in the alphabet , H becomes K , E becomes H , L becomes O
Why it's broken:
Only 25 possible keys (shift 1 through 25) , brute force takes seconds , frequency analysis breaks it instantly (E is most common in English)
Python Implementation:
def caesar_encrypt(text , shift):
result = ""
for char in text.upper():
if char.isalpha():
shifted = (ord(char) - ord('A') + shift) % 26 + ord('A')
result += chr(shifted)
else:
result += char
return result
def caesar_decrypt(text , shift):
return caesar_encrypt(text , -shift)
print(caesar_encrypt("ATTACK AT DAWN" , 3))
Output: DWWDFN DW GDZQ
One-liner decrypt:
echo "KHOOR" | tr 'A-Z' 'X-ZA-W'
This shifts back 3 positions using tr command , quick and dirty Caesar decryption
Brute Force Attack:
def caesar_bruteforce(ciphertext):
for shift in range(26):
print(f"Shift {shift}: {caesar_decrypt(ciphertext , shift)}")
caesar_bruteforce("KHOOR ZRUOG")
Tries all 25 possible shifts , one of them will be readable English
Substitution Ciphers¶
Replace each letter with another letter
More complex than Caesar , uses a full substitution alphabet instead of just shifting , each letter maps to a different letter
Example Mapping:
Plain: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Cipher: ZEBRASCDFGHIJKLMNOPQTUVWXY
A becomes Z , B becomes E , C becomes B , and so on
Why it's broken:
Frequency analysis destroys it , E is most common in English (12.7%) , if X appears 12.7% in ciphertext , X probably represents E
Python Implementation:
def substitution_encrypt(text , key):
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result = ""
for char in text.upper():
if char in alphabet:
result += key[alphabet.index(char)]
else:
result += char
return result
key = "ZEBRASCDFGHIJKLMNOPQTUVWXY"
print(substitution_encrypt("HELLO" , key))
Frequency Analysis Attack:
from collections import Counter
def frequency_analysis(ciphertext):
letters = [c for c in ciphertext.upper() if c.isalpha()]
freq = Counter(letters)
return freq.most_common(10)
ciphertext = "ITSSG VGKSR"
print(frequency_analysis(ciphertext))
Most common letter in ciphertext likely represents E , second most likely T , work backwards from there
Vigenère Cipher¶
Polyalphabetic substitution using a keyword
Instead of shifting by the same amount (Caesar) , shift by different amounts based on a repeating keyword , this was considered unbreakable for centuries
How it works:
Plaintext: ATTACKATDAWN
Key: LEMONLEMONLE
Ciphertext: LXFOPVEFRNHR
First letter: A + L = L , second letter: T + E = X , key repeats
Python Implementation:
def vigenere_encrypt(text , key):
result = ""
key = key.upper()
key_index = 0
for char in text.upper():
if char.isalpha():
shift = ord(key[key_index % len(key)]) - ord('A')
result += chr((ord(char) - ord('A') + shift) % 26 + ord('A'))
key_index += 1
else:
result += char
return result
def vigenere_decrypt(text , key):
result = ""
key = key.upper()
key_index = 0
for char in text.upper():
if char.isalpha():
shift = ord(key[key_index % len(key)]) - ord('A')
result += chr((ord(char) - ord('A') - shift) % 26 + ord('A'))
key_index += 1
else:
result += char
return result
print(vigenere_encrypt("ATTACK" , "LEMON"))
Breaking Vigenère:
Find key length using Kasiski examination or index of coincidence , once you know key length , split ciphertext into groups and use frequency analysis on each group
def kasiski_examination(ciphertext):
# Find repeated sequences
sequences = {}
for length in range(3 , 6):
for i in range(len(ciphertext) - length):
seq = ciphertext[i:i+length]
if seq in sequences:
sequences[seq].append(i)
else:
sequences[seq] = [i]
# Calculate distances between repetitions
for seq , positions in sequences.items():
if len(positions) > 1:
distances = [positions[i+1] - positions[i] for i in range(len(positions)-1)]
print(f"{seq}: distances {distances}")
Transposition Ciphers¶
Rearrange letters instead of substituting them
Unlike substitution ciphers that replace letters , transposition ciphers shuffle them around , same letters appear but in different positions
Rail Fence Cipher:
Plaintext: WEAREDISCOVEREDFLEEATONCE
Rails (3):
W . . . E . . . C . . . R . . . L . . . T . . . E
. E . R . D . S . O . E . E . F . E . A . O . C .
. . A . . . I . . . V . . . D . . . E . . . N . .
Ciphertext: WECRLTEERDSOEEFEAOCAIVDEN
Python Implementation:
def railfence_encrypt(text , rails):
fence = [[] for _ in range(rails)]
rail = 0
direction = 1
for char in text:
fence[rail].append(char)
rail += direction
if rail == 0 or rail == rails - 1:
direction *= -1
return ''.join([''.join(rail) for rail in fence])
print(railfence_encrypt("WEAREDISCOVERED" , 3))
Columnar Transposition:
Write message in rows , read in columns based on key order
Key: 4 3 1 2 5
Message: W E A R E
D I S C O
V E R E D
Read columns in order 1,2,3,4,5: ASREIEEDWVCRO
One-Time Pad¶
The only provably unbreakable cipher
XOR plaintext with a truly random key that's as long as the message , used once and destroyed , mathematically proven to be unbreakable if used correctly
How it works:
Plaintext: HELLO (binary: 01001000 01000101 01001100 01001100 01001111)
Random key: 10110101 11001010 00110011 10101010 11110000
XOR result: 11111101 10001111 01111111 11100110 10111111
Why it's unbreakable:
Every possible plaintext is equally likely , no frequency analysis works , no pattern exists , attacker gains zero information
Why nobody uses it:
- Key must be truly random (hard to generate)
- Key must be as long as message (impractical for large data)
- Key must never be reused (key management nightmare)
- Key must be securely distributed (defeats the purpose)
Python Implementation:
import os
def otp_encrypt(plaintext):
key = os.urandom(len(plaintext))
ciphertext = bytes([p ^ k for p , k in zip(plaintext , key)])
return ciphertext , key
def otp_decrypt(ciphertext , key):
return bytes([c ^ k for c , k in zip(ciphertext , key)])
message = b"SECRET"
ciphertext , key = otp_encrypt(message)
print(f"Ciphertext: {ciphertext.hex()}")
print(f"Decrypted: {otp_decrypt(ciphertext , key)}")
Real-world usage:
Soviet spy agencies used OTP for decades , Moscow-Washington hotline used it , modern systems don't because key distribution is impossible at scale
Mathematical Foundations¶
The math that makes modern crypto work
You don't need a PhD in mathematics to use crypto , but understanding the basics helps you understand why things work and when they break
Core Concepts:
- Modular arithmetic: clock math where numbers wrap around
- Prime numbers: building blocks of RSA and Diffie-Hellman
- One-way functions: easy to compute , impossible to reverse
- Trapdoor functions: easy with secret , hard without it
Modular Arithmetic¶
Clock math that wraps numbers around
Think of a 12-hour clock , 13 o'clock is 1 o'clock , 25 o'clock is 1 o'clock , that's modular arithmetic
Notation:
13 mod 12 = 1
25 mod 12 = 1
100 mod 7 = 2
Properties:
(a + b) mod m = ((a mod m) + (b mod m)) mod m
(a * b) mod m = ((a mod m) * (b mod m)) mod m
(a ^ b) mod m = computed using modular exponentiation
Modular Inverse:
Number x where (a * x) mod m = 1
Example: inverse of 3 mod 11 is 4 because (3 * 4) mod 11 = 12 mod 11 = 1
Python Implementation:
def mod_inverse(a , m):
# Extended Euclidean Algorithm
m0 , x0 , x1 = m , 0 , 1
while a > 1:
q = a // m
m , a = a % m , m
x0 , x1 = x1 - q * x0 , x0
return x1 + m0 if x1 < 0 else x1
print(mod_inverse(3 , 11)) # Output: 4
Modular Exponentiation:
Compute (base ^ exp) mod m efficiently without calculating the full power
def mod_pow(base , exp , mod):
result = 1
base %= mod
while exp > 0:
if exp & 1:
result = (result * base) % mod
base = (base * base) % mod
exp >>= 1
return result
print(mod_pow(2 , 100 , 1000)) # Output: 376
This is the core operation in RSA encryption and decryption
Prime Numbers¶
The foundation of public key cryptography
Prime numbers are only divisible by 1 and themselves , 2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , and so on
Why Primes Matter:
- RSA security relies on difficulty of factoring large primes
- Diffie-Hellman uses prime modulus for discrete logarithm
- Elliptic curves use prime field arithmetic
Primality Testing:
Miller-Rabin test is probabilistic but fast , runs k rounds to increase confidence
import random
def miller_rabin(n , k=5):
if n < 2:
return False
if n == 2 or n == 3:
return True
if n % 2 == 0:
return False
# Write n-1 as 2^r * d
r , d = 0 , n - 1
while d % 2 == 0:
r += 1
d //= 2
# Witness loop
for _ in range(k):
a = random.randint(2 , n - 2)
x = pow(a , d , n)
if x == 1 or x == n - 1:
continue
for _ in range(r - 1):
x = pow(x , 2 , n)
if x == n - 1:
break
else:
return False
return True
print(miller_rabin(97)) # True
print(miller_rabin(100)) # False
Generating Large Primes:
def generate_prime(bits):
while True:
candidate = random.getrandbits(bits)
candidate |= (1 << bits - 1) | 1 # Set MSB and LSB
if miller_rabin(candidate):
return candidate
prime = generate_prime(512)
print(f"512-bit prime: {prime}")
Command to generate prime:
openssl prime -generate -bits 512
Generates a 512-bit prime number using OpenSSL
Euler's Theorem¶
Foundation of RSA mathematics
Euler's theorem states: if gcd(a,n) = 1 then a^φ(n) ≡ 1 (mod n)
Where φ(n) is Euler's totient function counting numbers less than n that are coprime to n
For prime p:
φ(p) = p - 1
For two primes p and q:
φ(p*q) = (p-1)(q-1)
Why RSA works:
m^(e*d) ≡ m (mod n)
Because: e*d ≡ 1 (mod φ(n))
So: e*d = k*φ(n) + 1
Therefore: m^(e*d) = m^(k*φ(n)+1) = (m^φ(n))^k * m ≡ 1^k * m ≡ m (mod n)
Python Implementation:
def euler_totient(n):
result = n
p = 2
while p * p <= n:
if n % p == 0:
while n % p == 0:
n //= p
result -= result // p
p += 1
if n > 1:
result -= result // n
return result
print(euler_totient(36)) # Output: 12
print(euler_totient(17)) # Output: 16 (prime-1)
Discrete Logarithm¶
The hard problem behind Diffie-Hellman
Given g^x ≡ h (mod p) , finding x is the discrete logarithm problem
Easy direction: compute g^x mod p using modular exponentiation Hard direction: given g, h, p find x (no efficient algorithm known)
Example:
g = 5, p = 23
5^6 mod 23 = 8 (easy to compute)
Given: 5^x ≡ 8 (mod 23), find x
Answer: x = 6 (hard to find without trying all values)
Baby-Step Giant-Step Algorithm:
import math
def discrete_log(g, h, p):
n = int(math.sqrt(p)) + 1
# Baby steps: compute g^j for j in [0, n)
baby = {}
power = 1
for j in range(n):
baby[power] = j
power = (power * g) % p
# Giant steps: compute h * (g^-n)^i
factor = pow(g, n * (p - 2), p) # g^-n mod p
gamma = h
for i in range(n):
if gamma in baby:
return i * n + baby[gamma]
gamma = (gamma * factor) % p
return None
print(discrete_log(5, 8, 23)) # Output: 6
This algorithm has complexity O(√p) instead of O(p)
Chinese Remainder Theorem¶
Solving simultaneous modular equations
Given system:
x ≡ a1 (mod n1)
x ≡ a2 (mod n2)
...
x ≡ ak (mod nk)
If all ni are pairwise coprime, unique solution exists modulo N = n1*n2*...*nk
Example:
x ≡ 2 (mod 3)
x ≡ 3 (mod 5)
x ≡ 2 (mod 7)
Solution: x = 23
Python Implementation:
def crt(remainders, moduli):
total = 0
prod = 1
for m in moduli:
prod *= m
for r, m in zip(remainders, moduli):
p = prod // m
total += r * mod_inverse(p, m) * p
return total % prod
remainders = [2, 3, 2]
moduli = [3, 5, 7]
print(crt(remainders, moduli)) # Output: 23
Used in RSA-CRT:
Speeds up RSA decryption by computing modulo p and q separately then combining
Fermat's Little Theorem¶
Special case of Euler's theorem for primes
If p is prime and a is not divisible by p:
a^(p-1) ≡ 1 (mod p)
Useful corollary:
a^p ≡ a (mod p)
Computing modular inverse:
a^-1 ≡ a^(p-2) (mod p)
Python example:
def fermat_inverse(a, p):
return pow(a, p - 2, p)
print(fermat_inverse(3, 11)) # Output: 4
print((3 * 4) % 11) # Verify: 1
Primality test:
If a^(n-1) ≡ 1 (mod n) for random a, n is probably prime
def fermat_test(n, k=5):
if n < 2:
return False
for _ in range(k):
a = random.randint(2, n - 2)
if pow(a, n - 1, n) != 1:
return False
return True
Note: Carmichael numbers fool this test, use Miller-Rabin instead
Symmetric Encryption Basics¶
Same key encrypts and decrypts
Symmetric crypto is fast, efficient, perfect for bulk data, but has one problem: how do you share the key securely?
Key characteristics:
- Single shared secret key
- Fast encryption/decryption (hardware acceleration available)
- Small key sizes (128-256 bits)
- Key distribution problem (need secure channel)
Two main types:
Stream ciphers: encrypt bit-by-bit or byte-by-byte Block ciphers: encrypt fixed-size blocks (usually 128 bits)
Common algorithms:
- AES: industry standard, hardware accelerated
- ChaCha20: modern stream cipher, software-friendly
- 3DES: legacy, being phased out
- Blowfish/Twofish: older alternatives
When to use:
- Encrypting files on disk
- Database encryption
- VPN tunnels
- TLS session encryption (after key exchange)
Python example with AES:
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
key = get_random_bytes(32) # 256-bit key
cipher = AES.new(key, AES.MODE_GCM)
nonce = cipher.nonce
plaintext = b"Secret message"
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
# Decrypt
decipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
decrypted = decipher.decrypt_and_verify(ciphertext, tag)
print(decrypted)
Stream Ciphers¶
Encrypt one bit or byte at a time
Stream ciphers generate a keystream (pseudo-random sequence) and XOR it with plaintext
How they work:
Plaintext: 01001000 01000101 01001100
Keystream: 10110101 11001010 00110011
Ciphertext: 11111101 10001111 01111111
Advantages:
- No padding needed
- Can encrypt data of any length
- Low memory footprint
- Fast in software
Disadvantages:
- Never reuse key/nonce pair (catastrophic failure)
- No error propagation (bit flip in ciphertext = bit flip in plaintext)
- Vulnerable to bit-flipping attacks without authentication
Modern stream ciphers:
- ChaCha20: used in TLS, SSH, WireGuard
- Salsa20: predecessor to ChaCha20
- AES-CTR: block cipher in counter mode (acts like stream cipher)
RC4 Cipher¶
Broken stream cipher still found in legacy systems
RC4 was designed in 1987, widely used in WEP and early TLS, now completely broken
How RC4 works:
- Key Scheduling Algorithm (KSA): initialize 256-byte state array
- Pseudo-Random Generation Algorithm (PRGA): generate keystream bytes
- XOR keystream with plaintext
Python implementation:
def rc4(key, data):
# KSA
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# PRGA
i = j = 0
keystream = []
for _ in range(len(data)):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
keystream.append(K)
return bytes([d ^ k for d, k in zip(data, keystream)])
key = b"secretkey"
plaintext = b"attack at dawn"
ciphertext = rc4(key, plaintext)
print(ciphertext.hex())
# Decrypt (same operation)
decrypted = rc4(key, ciphertext)
print(decrypted)
Why it's broken:
- Biased keystream (certain bytes more likely)
- Weak key scheduling
- Related-key attacks
- First bytes of keystream are non-random
Never use RC4 in new systems
ChaCha20¶
Modern stream cipher replacing RC4
Designed by Daniel Bernstein in 2008, now used in TLS 1.3, WireGuard, and SSH
Advantages over RC4:
- No known biases in keystream
- Constant-time implementation (resistant to timing attacks)
- Fast in software (no lookup tables)
- 256-bit key, 96-bit nonce
How it works:
ChaCha20 uses 20 rounds of quarter-round operations on a 512-bit state
State layout:
cccccccc cccccccc cccccccc cccccccc (constants)
kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk (key)
kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk (key)
bbbbbbbb nnnnnnnn nnnnnnnn nnnnnnnn (counter + nonce)
Python implementation (simplified):
def rotl(a, b):
return ((a << b) | (a >> (32 - b))) & 0xffffffff
def quarter_round(a, b, c, d):
a = (a + b) & 0xffffffff; d ^= a; d = rotl(d, 16)
c = (c + d) & 0xffffffff; b ^= c; b = rotl(b, 12)
a = (a + b) & 0xffffffff; d ^= a; d = rotl(d, 8)
c = (c + d) & 0xffffffff; b ^= c; b = rotl(b, 7)
return a, b, c, d
# Full ChaCha20 implementation is complex
# Use library: from Crypto.Cipher import ChaCha20
Using ChaCha20:
from Crypto.Cipher import ChaCha20
key = get_random_bytes(32)
nonce = get_random_bytes(12)
cipher = ChaCha20.new(key=key, nonce=nonce)
plaintext = b"Encrypted with ChaCha20"
ciphertext = cipher.encrypt(plaintext)
# Decrypt
decipher = ChaCha20.new(key=key, nonce=nonce)
decrypted = decipher.decrypt(ciphertext)
print(decrypted)
Command line encryption:
echo "secret message" | openssl enc -chacha20 -k "password" -pbkdf2 -base64
Salsa20¶
ChaCha20's predecessor
Similar design to ChaCha20 but with different quarter-round function
Differences from ChaCha20:
- Different diffusion pattern
- Slightly less secure (ChaCha20 improves diffusion)
- Still secure for most applications
Variants:
- Salsa20/20: full 20 rounds
- Salsa20/12: 12 rounds (faster, still secure)
- Salsa20/8: 8 rounds (fastest, reduced security margin)
Python with Salsa20:
from Crypto.Cipher import Salsa20
key = get_random_bytes(32)
cipher = Salsa20.new(key=key)
nonce = cipher.nonce
ciphertext = cipher.encrypt(b"Message")
decipher = Salsa20.new(key=key, nonce=nonce)
print(decipher.decrypt(ciphertext))
Block Ciphers¶
Encrypt fixed-size blocks of data
Block ciphers operate on fixed-size blocks (usually 128 bits), applying multiple rounds of substitution and permutation
Core concepts:
- Block size: typically 128 bits (16 bytes)
- Key size: 128, 192, or 256 bits
- Rounds: multiple iterations (10-14 for AES)
- Padding: required when plaintext isn't multiple of block size
Confusion and Diffusion:
- Confusion: relationship between ciphertext and key is complex
- Diffusion: changing one bit of plaintext affects many bits of ciphertext
Common block ciphers:
- AES (Rijndael): current standard
- DES: obsolete (56-bit key too small)
- 3DES: legacy (slow, being phased out)
- Blowfish: older alternative
- Twofish: AES finalist
DES and 3DES¶
Legacy ciphers being phased out
DES (Data Encryption Standard) was the standard from 1977 to 2001, now broken due to 56-bit key size
DES structure:
- 64-bit block size
- 56-bit key (8 bytes with parity bits)
- 16 rounds of Feistel network
- Broken by brute force in 1998
3DES (Triple DES):
Apply DES three times with different keys:
Ciphertext = DES_encrypt(K3, DES_decrypt(K2, DES_encrypt(K1, Plaintext)))
Effective key length: 168 bits (3 × 56)
Python DES example:
from Crypto.Cipher import DES3
key = b'DESCRYPT' # 8 bytes for DES
# For 3DES, need 24 bytes
key3 = DES3.adjust_key_parity(b'TWENTYFOUR_BYTE_KEY!!!!!')
cipher = DES3.new(key3, DES3.MODE_ECB)
plaintext = b'8bytesss'
ciphertext = cipher.encrypt(plaintext)
print(ciphertext.hex())
Why not to use DES:
- 56-bit keys broken by brute force
- 64-bit blocks vulnerable to birthday attacks
- Slow compared to AES
- No hardware acceleration on modern CPUs
3DES deprecation:
NIST deprecated 3DES in 2023, migrate to AES
AES Algorithm¶
The encryption standard everyone uses
AES (Advanced Encryption Standard) replaced DES in 2001, now the global standard for symmetric encryption
Key features:
- Block size: 128 bits (16 bytes)
- Key sizes: 128, 192, or 256 bits
- Rounds: 10 (AES-128), 12 (AES-192), 14 (AES-256)
- Hardware acceleration on modern CPUs (AES-NI)
Why AES won:
- Fast in both hardware and software
- No known practical attacks
- Simple structure (easy to implement)
- Flexible key sizes
AES operations:
- SubBytes: substitute bytes using S-box
- ShiftRows: shift rows cyclically
- MixColumns: mix columns using matrix multiplication
- AddRoundKey: XOR with round key
Python AES encryption:
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
key = get_random_bytes(16) # 128-bit key
cipher = AES.new(key, AES.MODE_ECB)
plaintext = b"Hello AES"
padded = pad(plaintext, AES.block_size)
ciphertext = cipher.encrypt(padded)
decipher = AES.new(key, AES.MODE_ECB)
decrypted = unpad(decipher.decrypt(ciphertext), AES.block_size)
print(decrypted)
Command line AES:
# Encrypt file
openssl enc -aes-256-cbc -salt -in file.txt -out file.enc -k password
# Decrypt file
openssl enc -d -aes-256-cbc -in file.enc -out file.txt -k password
AES Internals¶
How AES actually works under the hood
AES operates on a 4×4 matrix of bytes called the state
State representation:
Plaintext: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
State matrix:
00 44 88 cc
11 55 99 dd
22 66 aa ee
33 77 bb ff
SubBytes transformation:
Replace each byte using S-box (substitution box)
# Simplified S-box (first 16 values)
sbox = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76
]
def sub_bytes(state):
return [[sbox[byte] for byte in row] for row in state]
ShiftRows transformation:
Row 0: no shift
Row 1: shift left 1
Row 2: shift left 2
Row 3: shift left 3
Before: After:
00 44 88 cc 00 44 88 cc
11 55 99 dd 55 99 dd 11
22 66 aa ee aa ee 22 66
33 77 bb ff ff 33 77 bb
MixColumns transformation:
Multiply each column by fixed matrix in GF(2^8)
[02 03 01 01] [s0]
[01 02 03 01] × [s1]
[01 01 02 03] [s2]
[03 01 01 02] [s3]
AddRoundKey:
XOR state with round key derived from main key
def add_round_key(state, round_key):
return [[state[i][j] ^ round_key[i][j] for j in range(4)] for i in range(4)]
Key expansion:
Generate round keys from main key using Rijndael key schedule
For AES-128: 128-bit key expands to 11 round keys (176 bytes total)
Block Cipher Modes¶
How to encrypt data longer than one block
Block ciphers only encrypt fixed-size blocks, modes of operation handle multiple blocks
Common modes:
- ECB: Electronic Codebook (insecure, don't use)
- CBC: Cipher Block Chaining
- CTR: Counter mode
- GCM: Galois/Counter Mode (authenticated)
- CFB: Cipher Feedback
- OFB: Output Feedback
ECB Mode¶
Electronic Codebook - the insecure mode
Each block encrypted independently with same key
C1 = E(K, P1)
C2 = E(K, P2)
C3 = E(K, P3)
Why it's broken:
Identical plaintext blocks produce identical ciphertext blocks, patterns visible
Famous example: ECB penguin image shows original structure
Python ECB:
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(plaintext, 16))
Never use ECB for anything except testing
CBC Mode¶
Cipher Block Chaining - XOR with previous ciphertext
Each plaintext block XORed with previous ciphertext before encryption
C1 = E(K, P1 ⊕ IV)
C2 = E(K, P2 ⊕ C1)
C3 = E(K, P3 ⊕ C2)
Advantages:
- Identical blocks produce different ciphertext
- Error propagation (good for integrity)
- Widely supported
Disadvantages:
- Sequential (can't parallelize encryption)
- Padding oracle attacks possible
- Requires random IV
Python CBC:
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
key = get_random_bytes(16)
iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext, 16))
# Decrypt
decipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(decipher.decrypt(ciphertext), 16)
Command line CBC:
openssl enc -aes-256-cbc -in file.txt -out file.enc -K <hex_key> -iv <hex_iv>
CTR Mode¶
Counter mode - turns block cipher into stream cipher
Encrypt counter values and XOR with plaintext
C1 = P1 ⊕ E(K, Counter + 1)
C2 = P2 ⊕ E(K, Counter + 2)
C3 = P3 ⊕ E(K, Counter + 3)
Advantages:
- Parallelizable (encrypt/decrypt)
- No padding needed
- Random access (can decrypt any block)
- Preprocessing possible
Disadvantages:
- Never reuse nonce+key pair
- No integrity protection
- Bit-flipping attacks possible
Python CTR:
from Crypto.Cipher import AES
from Crypto.Util import Counter
key = get_random_bytes(16)
ctr = Counter.new(128)
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
ciphertext = cipher.encrypt(plaintext) # No padding needed
# Decrypt
ctr2 = Counter.new(128)
decipher = AES.new(key, AES.MODE_CTR, counter=ctr2)
decrypted = decipher.decrypt(ciphertext)
GCM Mode¶
Galois/Counter Mode - authenticated encryption
Combines CTR mode encryption with GMAC authentication
Ciphertext = CTR_encrypt(Plaintext)
Tag = GMAC(Ciphertext || AAD)
Advantages:
- Authenticated encryption (detects tampering)
- Parallelizable
- Hardware acceleration available
- Industry standard (TLS 1.3 uses it)
Components:
- Encryption: CTR mode
- Authentication: GMAC (Galois Message Authentication Code)
- Tag: 128-bit authentication tag
Python GCM:
cipher = AES.new(key, AES.MODE_GCM)
nonce = cipher.nonce
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
# Decrypt and verify
decipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
decrypted = decipher.decrypt_and_verify(ciphertext, tag)
With additional authenticated data:
cipher = AES.new(key, AES.MODE_GCM)
cipher.update(b"header data") # AAD not encrypted but authenticated
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
Padding Schemes¶
Making plaintext fit block size
Block ciphers need input to be multiple of block size, padding fills the gap
PKCS#7 padding:
Pad with bytes indicating pad length
Plaintext: "HELLO" (5 bytes)
Block size: 8 bytes
Padding: 03 03 03
Result: "HELLO\x03\x03\x03"
Python PKCS#7:
def pkcs7_pad(data, block_size):
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len] * pad_len)
def pkcs7_unpad(data):
pad_len = data[-1]
return data[:-pad_len]
padded = pkcs7_pad(b"HELLO", 8)
print(padded) # b'HELLO\x03\x03\x03'
ISO/IEC 7816-4 padding:
Pad with 0x80 followed by zeros
Plaintext: "HELLO"
Padding: 80 00 00
Result: "HELLO\x80\x00\x00"
Zero padding:
Pad with zeros (ambiguous if plaintext ends with zero)
ANSI X.923:
Pad with zeros, last byte is pad length
Plaintext: "HELLO"
Padding: 00 00 03
Result: "HELLO\x00\x00\x03"
Using library padding:
from Crypto.Util.Padding import pad, unpad
padded = pad(b"HELLO", 16)
original = unpad(padded, 16)
This completes Part 2 (Topics 16-30: Symmetric Cryptography)
Public Key Cryptography¶
Two keys instead of one
Asymmetric crypto uses a key pair: public key (anyone can have it) and private key (only you have it)
Core concept:
- Encrypt with public key , decrypt with private key
- Sign with private key , verify with public key
- Solves key distribution problem
- Enables digital signatures
Mathematical foundation:
Based on hard mathematical problems: * RSA: factoring large numbers * Diffie-Hellman: discrete logarithm * ECC: elliptic curve discrete logarithm
Advantages:
- No need to share secret keys
- Digital signatures prove authenticity
- Key exchange without prior communication
- Non-repudiation (can't deny signing)
Disadvantages:
- Slow (100-1000x slower than symmetric)
- Larger key sizes (2048-4096 bits)
- More complex mathematics
- Vulnerable to quantum computers
Typical workflow:
- Generate key pair (public + private)
- Share public key freely
- Keep private key secret
- Others encrypt with your public key
- You decrypt with your private key
Python key generation:
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
private_key = key.export_key()
public_key = key.publickey().export_key()
print(public_key.decode())
RSA Algorithm¶
The workhorse of public key crypto
RSA invented in 1977 by Rivest , Shamir , and Adleman , still widely used today despite being slow
How RSA works:
Based on the fact that multiplying primes is easy but factoring the product is hard
Easy: 61 × 53 = 3233
Hard: factor 3233 back to 61 and 53
With 2048-bit primes , factoring takes longer than the universe has existed
Key generation:
- Pick two large primes p and q
- Compute n = p × q (modulus)
- Compute φ(n) = (p-1)(q-1)
- Choose e such that gcd(e , φ(n)) = 1 (usually 65537)
- Compute d = e^-1 mod φ(n)
- Public key: (n , e)
- Private key: (n , d)
Encryption:
c = m^e mod n
Decryption:
m = c^d mod n
Why it works:
(m^e)^d = m^(ed) = m^(1 + kφ(n)) = m × (m^φ(n))^k ≡ m × 1^k ≡ m (mod n)
Python RSA implementation:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
# Generate keys
key = RSA.generate(2048)
public_key = key.publickey()
# Encrypt
cipher = PKCS1_OAEP.new(public_key)
ciphertext = cipher.encrypt(b"Secret message")
# Decrypt
decipher = PKCS1_OAEP.new(key)
plaintext = decipher.decrypt(ciphertext)
print(plaintext)
RSA Key Generation¶
Creating secure RSA keys
Step-by-step process:
import random
from math import gcd
def generate_rsa_keys(bits=2048):
# Step 1: Generate two large primes
p = generate_prime(bits // 2)
q = generate_prime(bits // 2)
# Step 2: Compute modulus
n = p * q
# Step 3: Compute totient
phi = (p - 1) * (q - 1)
# Step 4: Choose public exponent
e = 65537 # Common choice
# Step 5: Compute private exponent
d = mod_inverse(e , phi)
return (n , e) , (n , d)
public , private = generate_rsa_keys()
print(f"Public key: {public}")
print(f"Private key: {private}")
Security considerations:
- Use at least 2048 bits (3072 or 4096 for long-term)
- Primes must be random and independent
- p and q should be similar in size
- Check that gcd(e , φ(n)) = 1
- Destroy p , q , and φ(n) after key generation
Command to generate RSA keys:
# Generate 2048-bit private key
openssl genrsa -out private.pem 2048
# Extract public key
openssl rsa -in private.pem -pubout -out public.pem
# View key details
openssl rsa -in private.pem -text -noout
Key formats:
- PEM: Base64-encoded with headers
- DER: Binary format
- PKCS#1: RSA-specific format
- PKCS#8: Generic private key format
RSA Encryption¶
Encrypting messages with RSA
Textbook RSA (insecure):
def rsa_encrypt(m , e , n):
return pow(m , e , n)
def rsa_decrypt(c , d , n):
return pow(c , d , n)
# Example with small numbers
n , e = 3233 , 17
n , d = 3233 , 2753
message = 123
ciphertext = rsa_encrypt(message , e , n)
decrypted = rsa_decrypt(ciphertext , d , n)
print(f"Original: {message} , Decrypted: {decrypted}")
Why textbook RSA is broken:
- Deterministic (same message = same ciphertext)
- Malleable (attacker can modify ciphertext)
- Small message attack (if m^e < n)
- No integrity protection
PKCS#1 v1.5 padding:
Add random padding before encryption
EM = 0x00 || 0x02 || PS || 0x00 || M
Where:
- PS = random non-zero bytes
- Length of EM = key size
OAEP padding (better):
Optimal Asymmetric Encryption Padding
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
cipher = PKCS1_OAEP.new(key.publickey())
ciphertext = cipher.encrypt(b"Message")
decipher = PKCS1_OAEP.new(key)
plaintext = decipher.decrypt(ciphertext)
Hybrid encryption:
Use RSA to encrypt symmetric key , then use symmetric key for data
from Crypto.Cipher import AES , PKCS1_OAEP
from Crypto.Random import get_random_bytes
# Generate random AES key
aes_key = get_random_bytes(32)
# Encrypt AES key with RSA
rsa_cipher = PKCS1_OAEP.new(public_key)
encrypted_key = rsa_cipher.encrypt(aes_key)
# Encrypt data with AES
aes_cipher = AES.new(aes_key , AES.MODE_GCM)
ciphertext , tag = aes_cipher.encrypt_and_digest(large_data)
RSA Signatures¶
Proving authenticity with RSA
How signing works:
- Hash the message
- Encrypt hash with private key (signature)
- Anyone can decrypt with public key
- Compare decrypted hash with actual hash
Python RSA signature:
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
message = b"Sign this message"
# Sign
h = SHA256.new(message)
signature = pkcs1_15.new(key).sign(h)
# Verify
try:
h = SHA256.new(message)
pkcs1_15.new(key.publickey()).verify(h , signature)
print("Signature valid")
except:
print("Signature invalid")
PSS padding (better than PKCS#1):
Probabilistic Signature Scheme
from Crypto.Signature import pss
# Sign with PSS
h = SHA256.new(message)
signature = pss.new(key).sign(h)
# Verify
try:
h = SHA256.new(message)
pss.new(key.publickey()).verify(h , signature)
print("Valid")
except:
print("Invalid")
Command to sign file:
# Sign
openssl dgst -sha256 -sign private.pem -out signature.bin file.txt
# Verify
openssl dgst -sha256 -verify public.pem -signature signature.bin file.txt
Diffie-Hellman¶
Key exchange without prior secrets
Allows two parties to agree on a shared secret over an insecure channel
How it works:
Public parameters: prime p , generator g
Alice:
1. Choose secret a
2. Compute A = g^a mod p
3. Send A to Bob
Bob:
1. Choose secret b
2. Compute B = g^b mod p
3. Send B to Alice
Shared secret:
Alice: s = B^a mod p = (g^b)^a mod p = g^(ab) mod p
Bob: s = A^b mod p = (g^a)^b mod p = g^(ab) mod p
Python implementation:
import random
def diffie_hellman():
# Public parameters
p = 23 # Prime (use large prime in practice)
g = 5 # Generator
# Alice's side
a = random.randint(1 , p-1)
A = pow(g , a , p)
# Bob's side
b = random.randint(1 , p-1)
B = pow(g , b , p)
# Compute shared secret
secret_alice = pow(B , a , p)
secret_bob = pow(A , b , p)
print(f"Alice's secret: {secret_alice}")
print(f"Bob's secret: {secret_bob}")
assert secret_alice == secret_bob
diffie_hellman()
Security:
Based on discrete logarithm problem: given g , p , and g^x mod p , finding x is hard
Vulnerable to man-in-the-middle:
Attacker intercepts and replaces A and B , need authentication
Authenticated Diffie-Hellman:
Use certificates or pre-shared keys to authenticate parties
ECDH¶
Elliptic Curve Diffie-Hellman
Same concept as DH but using elliptic curves instead of modular arithmetic
Advantages over DH:
- Smaller keys (256-bit ECC ≈ 3072-bit RSA)
- Faster computation
- Less bandwidth
- Same security level
How ECDH works:
Public parameters: elliptic curve E , base point G
Alice:
1. Choose secret a
2. Compute A = a × G (point multiplication)
3. Send A to Bob
Bob:
1. Choose secret b
2. Compute B = b × G
3. Send B to Alice
Shared secret:
Alice: S = a × B = a × (b × G) = (ab) × G
Bob: S = b × A = b × (a × G) = (ab) × G
Python ECDH:
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
# Generate private keys
alice_private = ec.generate_private_key(ec.SECP256R1())
bob_private = ec.generate_private_key(ec.SECP256R1())
# Get public keys
alice_public = alice_private.public_key()
bob_public = bob_private.public_key()
# Compute shared secret
alice_shared = alice_private.exchange(ec.ECDH() , bob_public)
bob_shared = bob_private.exchange(ec.ECDH() , alice_public)
# Derive key from shared secret
kdf = HKDF(algorithm=hashes.SHA256() , length=32 , salt=None , info=b'')
alice_key = kdf.derive(alice_shared)
bob_key = kdf.derive(bob_shared)
print(alice_key == bob_key) # True
Command to generate ECDH keys:
# Generate private key
openssl ecparam -name secp256k1 -genkey -noout -out ec_private.pem
# Extract public key
openssl ec -in ec_private.pem -pubout -out ec_public.pem
Elliptic Curves¶
The math behind modern crypto
Elliptic curves are algebraic curves defined by equation: y² = x³ + ax + b
Point addition:
Given two points P and Q on curve , P + Q is also on curve
Point doubling:
2P = P + P
Scalar multiplication:
nP = P + P + ... + P (n times)
Why curves work for crypto:
- Point addition is easy
- Scalar multiplication is easy
- Discrete logarithm is hard (given P and Q = nP , finding n is hard)
Python point operations:
def point_add(P , Q , a , p):
if P == (0 , 0):
return Q
if Q == (0 , 0):
return P
x1 , y1 = P
x2 , y2 = Q
if x1 == x2 and y1 == (-y2) % p:
return (0 , 0) # Point at infinity
if P == Q:
# Point doubling
m = (3 * x1 * x1 + a) * pow(2 * y1 , -1 , p) % p
else:
# Point addition
m = (y2 - y1) * pow(x2 - x1 , -1 , p) % p
x3 = (m * m - x1 - x2) % p
y3 = (m * (x1 - x3) - y1) % p
return (x3 , y3)
# Example on curve y² = x³ + 7 (mod 17)
P = (5 , 1)
Q = (6 , 3)
R = point_add(P , Q , 0 , 17)
print(f"P + Q = {R}")
Curve25519¶
Modern elliptic curve for ECDH
Designed by Daniel Bernstein for high security and performance
Properties:
- 128-bit security level
- Fast constant-time implementation
- Resistant to side-channel attacks
- No known patents
- Used in Signal , WireGuard , SSH
Curve equation:
y² = x³ + 486662x² + x (mod 2^255 - 19)
Python with Curve25519:
from cryptography.hazmat.primitives.asymmetric import x25519
# Generate keys
private_key = x25519.X25519PrivateKey.generate()
public_key = private_key.public_key()
# Peer's public key
peer_public = x25519.X25519PublicKey.from_public_bytes(peer_public_bytes)
# Compute shared secret
shared_secret = private_key.exchange(peer_public)
print(shared_secret.hex())
Command line:
# Generate Curve25519 key
openssl genpkey -algorithm X25519 -out x25519_private.pem
# Extract public key
openssl pkey -in x25519_private.pem -pubout -out x25519_public.pem
secp256k1¶
Bitcoin's elliptic curve
Used in Bitcoin , Ethereum , and other cryptocurrencies
Curve equation:
y² = x³ + 7 (mod p)
Where p = 2^256 - 2^32 - 977
Properties:
- 128-bit security
- Koblitz curve (special structure)
- Efficient implementation
- Well-studied
Python with secp256k1:
import ecdsa
# Generate key
sk = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1)
vk = sk.verifying_key
# Sign message
message = b"Transaction data"
signature = sk.sign(message)
# Verify
try:
vk.verify(signature , message)
print("Valid signature")
except:
print("Invalid signature")
Bitcoin address generation:
import hashlib
def pubkey_to_address(pubkey):
# SHA-256
sha = hashlib.sha256(pubkey).digest()
# RIPEMD-160
ripe = hashlib.new('ripemd160' , sha).digest()
# Add version byte
versioned = b'\x00' + ripe
# Double SHA-256 for checksum
checksum = hashlib.sha256(hashlib.sha256(versioned).digest()).digest()[:4]
# Base58 encode
address = base58_encode(versioned + checksum)
return address
ECDSA¶
Elliptic Curve Digital Signature Algorithm
Standard for digital signatures using elliptic curves
How ECDSA works:
Signing:
1. Hash message: h = H(m)
2. Choose random k
3. Compute R = k × G
4. r = R.x mod n
5. s = k^-1 (h + r × private_key) mod n
6. Signature = (r , s)
Verification:
1. Hash message: h = H(m)
2. Compute u1 = h × s^-1 mod n
3. Compute u2 = r × s^-1 mod n
4. Compute R' = u1 × G + u2 × public_key
5. Verify r == R'.x mod n
Python ECDSA:
from ecdsa import SigningKey , SECP256k1
import hashlib
# Generate key
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
# Sign
message = b"Message to sign"
signature = sk.sign(message , hashfunc=hashlib.sha256)
# Verify
try:
vk.verify(signature , message , hashfunc=hashlib.sha256)
print("Valid")
except:
print("Invalid")
Critical: never reuse k
If k is reused for two signatures , private key can be recovered
Sony PlayStation 3 hack:
Sony reused k in ECDSA signatures , hackers recovered private key and signed their own code
EdDSA¶
Edwards-curve Digital Signature Algorithm
Modern alternative to ECDSA using Edwards curves
Advantages over ECDSA:
- Deterministic (no random k needed)
- Faster signing and verification
- Simpler implementation
- Resistant to side-channel attacks
- No known patents
Ed25519:
EdDSA using Curve25519
from cryptography.hazmat.primitives.asymmetric import ed25519
# Generate key
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
# Sign
message = b"Message"
signature = private_key.sign(message)
# Verify
try:
public_key.verify(signature , message)
print("Valid")
except:
print("Invalid")
Command line:
# Generate Ed25519 key
ssh-keygen -t ed25519 -f ed25519_key
# Sign file
openssl pkeyutl -sign -inkey ed25519_key -out signature.bin -rawin -in file.txt
ElGamal¶
Alternative to RSA for encryption and signatures
Based on Diffie-Hellman problem
ElGamal encryption:
Key generation:
1. Choose prime p and generator g
2. Choose private key x
3. Compute public key y = g^x mod p
Encryption:
1. Choose random k
2. Compute c1 = g^k mod p
3. Compute c2 = m × y^k mod p
4. Ciphertext = (c1 , c2)
Decryption:
1. Compute s = c1^x mod p
2. Compute m = c2 × s^-1 mod p
Python ElGamal:
def elgamal_keygen(p , g):
x = random.randint(1 , p-2)
y = pow(g , x , p)
return (p , g , y) , x
def elgamal_encrypt(m , public_key):
p , g , y = public_key
k = random.randint(1 , p-2)
c1 = pow(g , k , p)
c2 = (m * pow(y , k , p)) % p
return c1 , c2
def elgamal_decrypt(c1 , c2 , x , p):
s = pow(c1 , x , p)
s_inv = pow(s , -1 , p)
m = (c2 * s_inv) % p
return m
Disadvantage:
Ciphertext is twice the size of plaintext
DSA¶
Digital Signature Algorithm
US government standard for digital signatures
How DSA works:
Key generation:
1. Choose prime p and q (q divides p-1)
2. Choose generator g of order q
3. Choose private key x
4. Compute public key y = g^x mod p
Signing:
1. Hash message: h = H(m)
2. Choose random k
3. Compute r = (g^k mod p) mod q
4. Compute s = k^-1 (h + x × r) mod q
5. Signature = (r , s)
Verification:
1. Hash message: h = H(m)
2. Compute w = s^-1 mod q
3. Compute u1 = h × w mod q
4. Compute u2 = r × w mod q
5. Compute v = (g^u1 × y^u2 mod p) mod q
6. Verify v == r
Python DSA:
from Crypto.PublicKey import DSA
from Crypto.Signature import DSS
from Crypto.Hash import SHA256
# Generate key
key = DSA.generate(2048)
# Sign
h = SHA256.new(b"Message")
signer = DSS.new(key , 'fips-186-3')
signature = signer.sign(h)
# Verify
verifier = DSS.new(key.publickey() , 'fips-186-3')
try:
verifier.verify(h , signature)
print("Valid")
except:
print("Invalid")
Key Exchange Protocols¶
Securely establishing shared secrets
Static Diffie-Hellman:
Both parties have long-term DH keys
Ephemeral Diffie-Hellman:
Generate new keys for each session (forward secrecy)
Station-to-Station (STS):
Authenticated DH with signatures
Alice → Bob: g^a
Bob → Alice: g^b , Sign(g^b || g^a)
Alice → Bob: Sign(g^a || g^b)
MQV (Menezes-Qu-Vanstone):
Combines static and ephemeral keys
X3DH (Extended Triple Diffie-Hellman):
Used in Signal protocol
Alice has: Identity key IK_A , Signed prekey SPK_A , One-time prekey OPK_A
Bob has: Identity key IK_B , Ephemeral key EK_B
Shared secret = DH(IK_A , SPK_B) || DH(EK_A , IK_B) || DH(EK_A , SPK_B) || DH(EK_A , OPK_B)
Python X3DH simulation:
from cryptography.hazmat.primitives.asymmetric import x25519
# Alice's keys
alice_ik = x25519.X25519PrivateKey.generate()
alice_ek = x25519.X25519PrivateKey.generate()
# Bob's keys
bob_ik = x25519.X25519PrivateKey.generate()
bob_spk = x25519.X25519PrivateKey.generate()
bob_opk = x25519.X25519PrivateKey.generate()
# Compute shared secrets
dh1 = alice_ik.exchange(bob_spk.public_key())
dh2 = alice_ek.exchange(bob_ik.public_key())
dh3 = alice_ek.exchange(bob_spk.public_key())
dh4 = alice_ek.exchange(bob_opk.public_key())
shared_secret = dh1 + dh2 + dh3 + dh4
print(f"Shared secret: {shared_secret.hex()}")
This completes Part 3 (Topics 31-45: Asymmetric Cryptography)
Hash Functions¶
One-way mathematical meat grinders
Hash functions take input of any size and produce fixed-size output , you can't reverse it back to the original
Properties:
- Deterministic: same input always gives same output
- Fixed size: output always same length regardless of input
- One-way: can't reverse hash back to input
- Collision resistant: hard to find two inputs with same hash
- Avalanche effect: tiny input change completely changes output
Common uses:
- Password storage (hash passwords , never store plaintext)
- Data integrity (verify files haven't been modified)
- Digital signatures (sign hash instead of entire message)
- Proof of work (Bitcoin mining)
- Hash tables (data structure indexing)
Python hash example:
import hashlib
data = b"Hash this message"
hash_value = hashlib.sha256(data).hexdigest()
print(f"SHA-256: {hash_value}")
# Verify integrity
received_data = b"Hash this message"
if hashlib.sha256(received_data).hexdigest() == hash_value:
print("Data intact")
Command line hashing:
# Hash file with SHA-256
sha256sum file.txt
# Hash string
echo -n "message" | sha256sum
# Verify checksum
sha256sum -c checksums.txt
MD5¶
Broken hash function still used for checksums
MD5 produces 128-bit hash , designed in 1991 , completely broken by 2004
Why MD5 is broken:
- Collision attacks practical (can create two files with same hash)
- Prefix collision attacks (can append data to create collision)
- Used in real attacks (Flame malware forged Microsoft certificates)
Python MD5:
import hashlib
hash_md5 = hashlib.md5(b"data").hexdigest()
print(hash_md5) # 32 hex characters (128 bits)
When MD5 is acceptable:
- Non-security checksums (verify download integrity)
- Hash table keys
- Deduplication (if collision doesn't matter)
Never use MD5 for:
- Password hashing
- Digital signatures
- Certificate verification
- Any security-critical application
Command:
md5sum file.txt
SHA Family¶
Secure Hash Algorithm family
SHA-1:
- 160-bit output
- Broken (collision found in 2017)
- Being phased out everywhere
- Don't use for new systems
SHA-2 family:
- SHA-224: 224-bit output
- SHA-256: 256-bit output (most common)
- SHA-384: 384-bit output
- SHA-512: 512-bit output
- All secure , widely used
SHA-3:
- Winner of NIST competition (2012)
- Different design from SHA-2 (Keccak)
- Not replacing SHA-2 , just alternative
- SHA3-256 , SHA3-512 variants
Python SHA-2:
import hashlib
# SHA-256
sha256 = hashlib.sha256(b"message").hexdigest()
print(f"SHA-256: {sha256}")
# SHA-512
sha512 = hashlib.sha512(b"message").hexdigest()
print(f"SHA-512: {sha512}")
Command line:
# SHA-256
echo -n "message" | sha256sum
# SHA-512
echo -n "message" | sha512sum
SHA-256¶
The standard cryptographic hash
256-bit output , part of SHA-2 family , used everywhere
How SHA-256 works:
- Pad message to multiple of 512 bits
- Initialize 8 hash values (constants)
- Process message in 512-bit chunks
- Each chunk goes through 64 rounds
- Final hash is concatenation of 8 values
Python implementation (simplified):
import hashlib
def sha256_hash(data):
return hashlib.sha256(data).hexdigest()
# Hash string
print(sha256_hash(b"Hello"))
# Hash file
with open('file.txt' , 'rb') as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
print(file_hash)
Incremental hashing:
hasher = hashlib.sha256()
hasher.update(b"part 1")
hasher.update(b"part 2")
hasher.update(b"part 3")
print(hasher.hexdigest())
Bitcoin mining:
Bitcoin uses double SHA-256: SHA256(SHA256(block_header))
def double_sha256(data):
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
SHA-3¶
The new generation hash function
Based on Keccak sponge construction , different from SHA-2
Sponge construction:
Input → Absorb phase → Squeeze phase → Output
State: 1600 bits
Rate: 1088 bits (for SHA3-256)
Capacity: 512 bits
Python SHA-3:
import hashlib
# SHA3-256
sha3_256 = hashlib.sha3_256(b"message").hexdigest()
print(sha3_256)
# SHA3-512
sha3_512 = hashlib.sha3_512(b"message").hexdigest()
print(sha3_512)
# SHAKE (extendable output)
shake128 = hashlib.shake_128(b"message").hexdigest(32) # 32 bytes output
print(shake128)
SHAKE variants:
Variable-length output functions
# SHAKE128 with 256-bit output
output = hashlib.shake_128(b"data").digest(32)
# SHAKE256 with 512-bit output
output = hashlib.shake_256(b"data").digest(64)
BLAKE2¶
Fast cryptographic hash function
Faster than SHA-2 , as secure as SHA-3 , optimized for modern CPUs
Variants:
- BLAKE2b: optimized for 64-bit platforms (up to 512-bit output)
- BLAKE2s: optimized for 32-bit platforms (up to 256-bit output)
Python BLAKE2:
import hashlib
# BLAKE2b (512-bit)
blake2b = hashlib.blake2b(b"message").hexdigest()
print(blake2b)
# BLAKE2s (256-bit)
blake2s = hashlib.blake2s(b"message").hexdigest()
print(blake2s)
# Keyed hashing (MAC)
mac = hashlib.blake2b(b"message" , key=b"secret").hexdigest()
print(mac)
Personalization:
# Different hash for different purposes
hash1 = hashlib.blake2b(b"data" , person=b"app1").hexdigest()
hash2 = hashlib.blake2b(b"data" , person=b"app2").hexdigest()
# hash1 != hash2
Command line:
# BLAKE2b
b2sum file.txt
# BLAKE2s
b2sum -a blake2s file.txt
Message Authentication Codes¶
Proving message authenticity with shared secret
MAC is like a hash but with a secret key , proves message came from someone with the key
Properties:
- Requires secret key
- Verifies authenticity and integrity
- Symmetric (same key for create and verify)
- Deterministic (same message + key = same MAC)
Difference from signatures:
- MAC: symmetric (shared secret)
- Signature: asymmetric (public/private keys)
- MAC: faster
- Signature: provides non-repudiation
Common MAC algorithms:
- HMAC: hash-based MAC
- CMAC: cipher-based MAC
- Poly1305: modern MAC
- GMAC: used in GCM mode
HMAC¶
Hash-based Message Authentication Code
Combines hash function with secret key
HMAC construction:
HMAC(K , m) = H((K ⊕ opad) || H((K ⊕ ipad) || m))
Where:
- K = secret key
- m = message
- H = hash function
- opad = 0x5c repeated
- ipad = 0x36 repeated
Python HMAC:
import hmac
import hashlib
key = b"secret_key"
message = b"message to authenticate"
# Create HMAC
mac = hmac.new(key , message , hashlib.sha256).hexdigest()
print(f"HMAC: {mac}")
# Verify HMAC
received_mac = mac
if hmac.compare_digest(mac , received_mac):
print("Authentic")
else:
print("Tampered")
Timing-safe comparison:
Always use hmac.compare_digest() to prevent timing attacks
# Bad (timing attack vulnerable)
if mac == received_mac:
pass
# Good (constant time)
if hmac.compare_digest(mac , received_mac):
pass
Command line:
# Generate HMAC
echo -n "message" | openssl dgst -sha256 -hmac "secret_key"
Poly1305¶
Fast one-time MAC
Designed by Daniel Bernstein , used with ChaCha20
Properties:
- Extremely fast (software optimized)
- 128-bit security
- One-time use (never reuse key)
- Used in ChaCha20-Poly1305
Python Poly1305:
from Crypto.Cipher import ChaCha20_Poly1305
key = get_random_bytes(32)
cipher = ChaCha20_Poly1305.new(key=key)
nonce = cipher.nonce
ciphertext , tag = cipher.encrypt_and_digest(b"message")
# Verify and decrypt
decipher = ChaCha20_Poly1305.new(key=key , nonce=nonce)
plaintext = decipher.decrypt_and_verify(ciphertext , tag)
Why one-time:
Reusing key allows attacker to forge MACs
SipHash¶
Fast short-input MAC
Designed for hash table protection against DoS attacks
Use cases:
- Hash table keys
- Bloom filters
- Network packet authentication
- Short message authentication
Python SipHash:
import siphash
key = b"0123456789ABCDEF" # 16 bytes
message = b"data"
hash_value = siphash.SipHash_2_4(key , message).hash()
print(hash_value)
Not for:
Long messages (use HMAC instead) Cryptographic signatures (use HMAC-SHA256)
Authenticated Encryption¶
Encryption + authentication in one operation
Provides both confidentiality and integrity
Why needed:
Encryption alone doesn't prevent tampering , attacker can modify ciphertext
AEAD (Authenticated Encryption with Associated Data):
- Encrypts plaintext
- Authenticates ciphertext
- Authenticates additional data (not encrypted)
Common AEAD modes:
- AES-GCM: most popular
- ChaCha20-Poly1305: modern alternative
- AES-CCM: used in WiFi
- AES-OCB: fastest but patented
Python AES-GCM:
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
key = get_random_bytes(32)
cipher = AES.new(key , AES.MODE_GCM)
nonce = cipher.nonce
# Additional authenticated data (not encrypted)
header = b"packet header"
cipher.update(header)
# Encrypt and authenticate
ciphertext , tag = cipher.encrypt_and_digest(b"secret data")
# Decrypt and verify
decipher = AES.new(key , AES.MODE_GCM , nonce=nonce)
decipher.update(header)
plaintext = decipher.decrypt_and_verify(ciphertext , tag)
ChaCha20-Poly1305:
from Crypto.Cipher import ChaCha20_Poly1305
cipher = ChaCha20_Poly1305.new(key=key)
cipher.update(b"header")
ciphertext , tag = cipher.encrypt_and_digest(b"message")
Key Derivation Functions¶
Turning passwords into keys
KDFs convert weak passwords into strong cryptographic keys
Why needed:
- Passwords are weak (low entropy)
- Need fixed-length keys
- Want to slow down brute force
- Add salt to prevent rainbow tables
Properties of good KDF:
- Slow (intentionally)
- Memory-hard (resist GPU attacks)
- Configurable work factor
- Salt support
Common KDFs:
- PBKDF2: standard , widely supported
- bcrypt: popular for passwords
- scrypt: memory-hard
- Argon2: modern , winner of PHC
PBKDF2¶
Password-Based Key Derivation Function 2
Standard KDF , widely supported , configurable iterations
How it works:
DK = PBKDF2(password , salt , iterations , key_length)
Internally:
- Repeatedly applies HMAC
- Each iteration makes brute force harder
Python PBKDF2:
import hashlib
from os import urandom
password = b"user_password"
salt = urandom(16)
iterations = 100000
key = hashlib.pbkdf2_hmac('sha256' , password , salt , iterations)
print(f"Derived key: {key.hex()}")
Storing passwords:
def hash_password(password):
salt = urandom(16)
key = hashlib.pbkdf2_hmac('sha256' , password.encode() , salt , 100000)
return salt + key
def verify_password(password , stored):
salt = stored[:16]
stored_key = stored[16:]
key = hashlib.pbkdf2_hmac('sha256' , password.encode() , salt , 100000)
return key == stored_key
Command line:
openssl enc -pbkdf2 -iter 100000 -aes-256-cbc -in file.txt -out file.enc
bcrypt¶
Password hashing designed to be slow
Built-in salt , adaptive work factor , resistant to GPU attacks
Python bcrypt:
import bcrypt
password = b"user_password"
# Hash password
hashed = bcrypt.hashpw(password , bcrypt.gensalt(rounds=12))
print(hashed)
# Verify password
if bcrypt.checkpw(password , hashed):
print("Correct password")
else:
print("Wrong password")
Work factor:
# Faster (2^10 iterations)
bcrypt.gensalt(rounds=10)
# Slower (2^14 iterations)
bcrypt.gensalt(rounds=14)
Stored format:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ │ │
│ │ └─ salt └─ hash
│ └─ cost factor (2^12)
└─ algorithm version
scrypt¶
Memory-hard key derivation function
Designed to resist GPU and ASIC attacks by requiring lots of memory
Parameters:
- N: CPU/memory cost (power of 2)
- r: block size
- p: parallelization factor
Python scrypt:
import hashlib
password = b"password"
salt = urandom(16)
# N=2^14 , r=8 , p=1
key = hashlib.scrypt(password , salt=salt , n=2**14 , r=8 , p=1 , dklen=32)
print(key.hex())
Memory usage:
Memory = 128 * N * r bytes
Example: N=2^14 , r=8
Memory = 128 * 16384 * 8 = 16 MB
Tuning:
# Low security (fast)
hashlib.scrypt(password , salt=salt , n=2**10 , r=8 , p=1)
# High security (slow)
hashlib.scrypt(password , salt=salt , n=2**18 , r=8 , p=1)
Argon2¶
Modern password hashing (PHC winner)
Designed in 2015 , resistant to GPU , FPGA , and ASIC attacks
Variants:
- Argon2d: data-dependent (faster , vulnerable to side-channels)
- Argon2i: data-independent (slower , side-channel resistant)
- Argon2id: hybrid (recommended)
Python Argon2:
from argon2 import PasswordHasher
ph = PasswordHasher()
# Hash password
hash_value = ph.hash("password")
print(hash_value)
# Verify password
try:
ph.verify(hash_value , "password")
print("Correct")
except:
print("Wrong")
Custom parameters:
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3 , # iterations
memory_cost=65536 , # 64 MB
parallelism=4 , # threads
hash_len=32 , # output length
salt_len=16 # salt length
)
Recommended settings (2024):
# Interactive (login)
time_cost=2 , memory_cost=65536 , parallelism=4
# Moderate
time_cost=3 , memory_cost=262144 , parallelism=4
# Sensitive (encryption keys)
time_cost=4 , memory_cost=1048576 , parallelism=4
Digital Signatures¶
Cryptographic proof of authenticity
Digital signature proves message came from specific person and hasn't been modified
How signatures work:
Signing:
1. Hash message
2. Encrypt hash with private key
3. Signature = encrypted hash
Verification:
1. Hash message
2. Decrypt signature with public key
3. Compare hashes
Properties:
- Authentication: proves who signed
- Integrity: detects modifications
- Non-repudiation: can't deny signing
Python RSA signature:
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
# Sign
message = b"Contract terms"
h = SHA256.new(message)
signature = pkcs1_15.new(key).sign(h)
# Verify
try:
pkcs1_15.new(key.publickey()).verify(h , signature)
print("Valid signature")
except:
print("Invalid signature")
ECDSA signature:
from ecdsa import SigningKey , SECP256k1
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
signature = sk.sign(b"message")
vk.verify(signature , b"message")
Certificate Authorities¶
Trust infrastructure for public keys
CAs issue digital certificates binding public keys to identities
Certificate structure:
Certificate:
- Subject: who owns the key
- Public key
- Issuer: who signed it
- Validity period
- Signature
X.509 certificate:
from cryptography import x509
from cryptography.hazmat.primitives import hashes
# Load certificate
with open('cert.pem' , 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read())
# Extract info
subject = cert.subject
issuer = cert.issuer
public_key = cert.public_key()
not_before = cert.not_valid_before
not_after = cert.not_valid_after
Command to view certificate:
# View certificate details
openssl x509 -in cert.pem -text -noout
# Verify certificate chain
openssl verify -CAfile ca.pem cert.pem
Self-signed certificate:
# Generate private key
openssl genrsa -out key.pem 2048
# Create self-signed certificate
openssl req -new -x509 -key key.pem -out cert.pem -days 365
This completes Part 4 (Topics 46-60: Hash Functions and MACs)
TLS Protocol¶
Transport Layer Security - securing the internet
TLS encrypts communication between client and server , used in HTTPS , email , VPNs
TLS handshake:
Client → Server: ClientHello (supported ciphers , random)
Server → Client: ServerHello (chosen cipher , random)
Server → Client: Certificate (public key)
Server → Client: ServerHelloDone
Client → Server: ClientKeyExchange (encrypted pre-master secret)
Client → Server: ChangeCipherSpec
Client → Server: Finished (encrypted)
Server → Client: ChangeCipherSpec
Server → Client: Finished (encrypted)
[Encrypted application data]
TLS 1.3 improvements:
- Faster handshake (1-RTT instead of 2-RTT)
- Forward secrecy mandatory
- Removed weak ciphers
- Encrypted handshake messages
Python TLS client:
import ssl
import socket
context = ssl.create_default_context()
with socket.create_connection(('google.com' , 443)) as sock:
with context.wrap_socket(sock , server_hostname='google.com') as ssock:
print(ssock.version())
print(ssock.cipher())
ssock.sendall(b'GET / HTTP/1.1\r\nHost: google.com\r\n\r\n')
print(ssock.recv(1024))
Command to test TLS:
# Check TLS version and cipher
openssl s_client -connect google.com:443 -tls1_3
# View certificate
openssl s_client -connect google.com:443 -showcerts
SSH Protocol¶
Secure Shell for remote access
SSH provides encrypted terminal access and file transfer
SSH connection:
1. TCP connection on port 22
2. Protocol version exchange
3. Key exchange (DH or ECDH)
4. Server authentication (host key)
5. User authentication (password or public key)
6. Encrypted session
Key exchange algorithms:
- diffie-hellman-group14-sha256
- ecdh-sha2-nistp256
- curve25519-sha256
Python SSH client:
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('hostname' , username='user' , key_filename='private_key')
stdin , stdout , stderr = client.exec_command('ls -la')
print(stdout.read().decode())
client.close()
Generate SSH keys:
# Ed25519 (recommended)
ssh-keygen -t ed25519 -C "your_email@example.com"
# RSA 4096-bit
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
# ECDSA
ssh-keygen -t ecdsa -b 521
SSH config hardening:
# /etc/ssh/sshd_config
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KexAlgorithms curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com
PGP and GPG¶
Pretty Good Privacy for email encryption
PGP encrypts emails and files using public key cryptography
GPG key generation:
# Generate key pair
gpg --full-generate-key
# List keys
gpg --list-keys
# Export public key
gpg --armor --export user@example.com > public.key
# Export private key
gpg --armor --export-secret-keys user@example.com > private.key
Encrypt and decrypt:
# Encrypt file
gpg --encrypt --recipient user@example.com file.txt
# Decrypt file
gpg --decrypt file.txt.gpg > file.txt
# Sign file
gpg --sign file.txt
# Verify signature
gpg --verify file.txt.gpg
Python GPG:
import gnupg
gpg = gnupg.GPG()
# Encrypt
encrypted = gpg.encrypt('message' , 'recipient@example.com')
print(encrypted)
# Decrypt
decrypted = gpg.decrypt(str(encrypted))
print(decrypted)
Post-Quantum Cryptography¶
Preparing for quantum computers
Quantum computers will break RSA , ECC , and Diffie-Hellman , need quantum-resistant algorithms
Quantum threats:
- Shor's algorithm: breaks RSA and ECC in polynomial time
- Grover's algorithm: weakens symmetric crypto (double key size)
Post-quantum algorithms:
- Lattice-based: CRYSTALS-Kyber (key exchange) , CRYSTALS-Dilithium (signatures)
- Hash-based: SPHINCS+ (signatures)
- Code-based: Classic McEliece (encryption)
- Multivariate: Rainbow (signatures)
NIST PQC winners (2022):
- CRYSTALS-Kyber: key encapsulation
- CRYSTALS-Dilithium: digital signatures
- SPHINCS+: stateless signatures
- FALCON: compact signatures
Python with Kyber:
from pqcrypto.kem.kyber512 import generate_keypair , encrypt , decrypt
# Generate keys
public_key , secret_key = generate_keypair()
# Encapsulate (create shared secret)
ciphertext , shared_secret = encrypt(public_key)
# Decapsulate (recover shared secret)
recovered_secret = decrypt(secret_key , ciphertext)
assert shared_secret == recovered_secret
Migration strategy:
- Hybrid approach: use both classical and PQC
- Increase symmetric key sizes (AES-128 → AES-256)
- Monitor NIST standardization
- Test PQC implementations
Zero Knowledge Proofs¶
Prove knowledge without revealing it
ZKP allows proving statement is true without revealing why
Example: password proof
Prove you know password without sending it
zk-SNARKs:
Zero-Knowledge Succinct Non-Interactive Argument of Knowledge
Used in Zcash cryptocurrency for private transactions
Applications:
- Anonymous credentials
- Private transactions (cryptocurrencies)
- Authentication without passwords
- Verifiable computation
Homomorphic Encryption¶
Computing on encrypted data
Perform operations on ciphertext that produce encrypted results
Types:
- Partially homomorphic: one operation (RSA multiplication , Paillier addition)
- Somewhat homomorphic: limited operations
- Fully homomorphic: unlimited operations
RSA multiplicative homomorphism:
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
e , n = key.e , key.n
# Encrypt two numbers
m1 , m2 = 5 , 7
c1 = pow(m1 , e , n)
c2 = pow(m2 , e , n)
# Multiply ciphertexts
c_product = (c1 * c2) % n
# Decrypt result
m_product = pow(c_product , key.d , n)
print(m_product) # 35 = 5 * 7
Applications:
- Cloud computing on encrypted data
- Private database queries
- Secure voting systems
- Medical data analysis
Cryptographic Best Practices¶
Don't roll your own crypto
Key management:
- Generate keys with cryptographically secure RNG
- Store keys in hardware security modules (HSM)
- Rotate keys periodically
- Destroy keys securely (overwrite memory)
- Never hardcode keys in source code
Algorithm selection:
- Symmetric: AES-256-GCM or ChaCha20-Poly1305
- Asymmetric: RSA-4096 or Ed25519
- Hashing: SHA-256 or BLAKE2
- Passwords: Argon2id or bcrypt
- Key exchange: ECDH with Curve25519
Common mistakes:
- Using ECB mode
- Reusing nonces
- Not authenticating ciphertext
- Weak random number generation
- Implementing crypto from scratch
Summary and Next Steps¶
You've covered 75 topics in cryptography
What you learned:
- Classical ciphers (Caesar , Vigenère , transposition)
- Mathematical foundations (modular arithmetic , primes , discrete log)
- Symmetric crypto (AES , ChaCha20 , block cipher modes)
- Asymmetric crypto (RSA , ECC , Diffie-Hellman)
- Hash functions (SHA-256 , SHA-3 , BLAKE2)
- MACs and authenticated encryption
- Key derivation (PBKDF2 , bcrypt , Argon2)
- Digital signatures and certificates
- Advanced topics (TLS , post-quantum , ZKP , homomorphic)
Linux crypto commands reference:
# Generate random data
head -c 32 /dev/urandom | base64
# Hash file
sha256sum file.txt
# Encrypt with AES
openssl enc -aes-256-gcm -pbkdf2 -in file.txt -out file.enc
# Generate SSH key
ssh-keygen -t ed25519
# Test TLS connection
openssl s_client -connect example.com:443
# View certificate
openssl x509 -in cert.pem -text -noout
# Generate self-signed cert
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes