Learning Goals
3 minBy the end of this lesson you can:
- Explain how a public/private key pair works and solves key distribution.
- Generate an RSA key pair and encrypt/decrypt with it in Python.
- Describe the maths intuition (one-way functions, factoring) without the heavy algebra.
- Explain why real systems encrypt a symmetric key with asymmetric crypto (hybrid).
Warm-Up · The Padlock Analogy
5 minBob publishes OPEN PADLOCKS (his public key) everywhere — anyone can take one. Aisha puts her message in a box and snaps Bob's padlock shut. Now ONLY Bob's KEY (his private key) can open it — not even Aisha can re-open it. The padlock travelled in the open; the key never left Bob.
Asymmetric (public-key) crypto uses a pair of keys: a public key you share with the world, and a private key you guard. What one encrypts, only the other decrypts. So anyone can encrypt to you using your public key, but only your private key reads it — no shared secret ever has to travel. This is what makes secure communication between strangers possible, and underlies HTTPS, SSH, and signing.
New Concept · Key Pairs, RSA & Hybrid
14 minTwo keys, two directions
ENCRYPT for privacy: anyone uses Bob's PUBLIC key → only Bob's PRIVATE reads it SIGN for authenticity: Bob uses his PRIVATE key → anyone verifies w/ PUBLIC (L8-17) Public key: share freely (it's meant to be public) Private key: NEVER share; losing it = identity theft; it stays on one machine
The maths intuition (no heavy algebra)
Asymmetric crypto rests on one-way functions: easy to compute forwards, infeasible to reverse without a secret. RSA uses factoring: multiplying two huge primes is easy, but factoring the product back into those primes is astronomically hard. The public key is built from the product; the private key needs the original primes. Elliptic-curve crypto (ECC) uses a different hard problem but the same principle. You don't need the equations — you need to know why it's secure: reversing it would require solving a problem no one can do quickly.
Generate an RSA key pair
from cryptography.hazmat.primitives.asymmetric import rsa private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) public_key = private_key.public_key() # 2048-bit is a reasonable minimum; 3072/4096 for longer-term security.
Encrypt with the public key, decrypt with the private
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes message = b"meet at noon" # anyone with the PUBLIC key can encrypt to the owner: ciphertext = public_key.encrypt( message, padding.OAEP( # OAEP = the safe padding scheme mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) # only the PRIVATE key can decrypt: plain = private_key.decrypt( ciphertext, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) print(plain.decode()) # "meet at noon"
Use OAEP padding (not the old PKCS#1 v1.5) — padding choice matters in RSA just like mode choice in AES. The library makes the safe option explicit.
⚠️ Asymmetric is slow and size-limited
RSA can only encrypt data smaller than the key (a 2048-bit key encrypts ~190 bytes with OAEP), and it's far slower than AES. So real systems use hybrid encryption: generate a random symmetric key (Fernet/AES), encrypt the actual data with that fast symmetric key, then encrypt just the small symmetric key with RSA. Best of both — RSA solves key distribution, AES does the bulk work. This is exactly how TLS, PGP, and encrypted email work.
Saving and loading keys (PEM)
from cryptography.hazmat.primitives import serialization # private key — encrypt it at rest with a passphrase! pem_private = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.BestAvailableEncryption(b"passphrase")) # public key — safe to share pem_public = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)
PEM is the standard text format (the -----BEGIN ... KEY----- blocks). Always store the private key encrypted with a passphrase, and guard it like the crown jewels.
Worked Example · Hybrid Encryption (the real pattern)
12 minGoal: encrypt a large message to Bob using only his public key — the hybrid scheme that real systems use. Aisha needs no shared secret; only Bob can read it.
import os from cryptography.fernet import Fernet from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes # Bob's key pair (he keeps private, publishes public) bob_priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) bob_pub = bob_priv.public_key() def encrypt_for(public_key, data: bytes) -> tuple[bytes, bytes]: # 1. fast symmetric key for the bulk data sym_key = Fernet.generate_key() encrypted_data = Fernet(sym_key).encrypt(data) # AES does the heavy lifting # 2. encrypt ONLY the small symmetric key with RSA (public key) encrypted_key = public_key.encrypt( sym_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) return encrypted_key, encrypted_data def decrypt_with(private_key, encrypted_key: bytes, encrypted_data: bytes) -> bytes: # 1. recover the symmetric key with the PRIVATE key sym_key = private_key.decrypt( encrypted_key, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) # 2. decrypt the bulk data with it return Fernet(sym_key).decrypt(encrypted_data) # Aisha encrypts a large message using only Bob's PUBLIC key: big_message = b"a very long secret report... " * 1000 # way over RSA's limit enc_key, enc_data = encrypt_for(bob_pub, big_message) print(f"encrypted {len(big_message)} bytes (RSA alone couldn't)") # Only Bob can decrypt, with his PRIVATE key: recovered = decrypt_with(bob_priv, enc_key, enc_data) print("round-trip ok:", recovered == big_message)
encrypted 29000 bytes (RSA alone couldn't) round-trip ok: True
Read the code
This is the pattern at the heart of secure communication. RSA can't encrypt 29KB directly — but it doesn't need to: it only encrypts the tiny 32-byte symmetric key, while fast AES (via Fernet) encrypts the actual report. Aisha used only Bob's public key — no shared secret travelled, solving last lesson's key-distribution problem. Only Bob's private key recovers the symmetric key and thus the data. TLS does exactly this in its handshake (Lesson 9). Recognise this hybrid shape and you understand most real-world encryption.
Try It Yourself
13 minGenerate an RSA key pair, encrypt a short message with the public key, decrypt with the private key. Then confirm the public key cannot decrypt (it's the wrong direction).
Try to RSA-encrypt a message larger than ~190 bytes with a 2048-bit key and observe it fail. Then encrypt the same large message with the hybrid scheme and succeed. This proves why hybrid exists.
Hint
try: bob_pub.encrypt(b"x" * 500, padding.OAEP( mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) except ValueError as e: print("RSA too small for this data:", e) # → use hybrid
Serialize the key pair to PEM (private key encrypted with a passphrase), write them to files, reload them, and complete a hybrid round-trip with the reloaded keys. Confirm loading the private key requires the passphrase.
Hint
from cryptography.hazmat.primitives import serialization priv = serialization.load_pem_private_key( open("priv.pem", "rb").read(), password=b"passphrase") # wrong pw → error pub = serialization.load_pem_public_key(open("pub.pem", "rb").read())
Mini-Challenge · Two-Person Secure Exchange
8 minSimulate Aisha and Bob each having their own key pair. Have Aisha send Bob a hybrid-encrypted message using only Bob's public key, and Bob reply using only Aisha's public key. Print what an eavesdropper who captured both ciphertexts (but neither private key) would see. This makes the key-distribution solution concrete.
Show a sample solution
from cryptography.hazmat.primitives.asymmetric import rsa def new_pair(): p = rsa.generate_private_key(public_exponent=65537, key_size=2048) return p, p.public_key() aisha_priv, aisha_pub = new_pair() bob_priv, bob_pub = new_pair() # Aisha → Bob (uses Bob's PUBLIC key only) ek, ed = encrypt_for(bob_pub, b"Bob, the meeting is at 3pm.") print("Bob reads:", decrypt_with(bob_priv, ek, ed).decode()) # Bob → Aisha (uses Aisha's PUBLIC key only) ek2, ed2 = encrypt_for(aisha_pub, b"Got it, see you then.") print("Aisha reads:", decrypt_with(aisha_priv, ek2, ed2).decode()) # An eavesdropper has both ciphertexts but NO private key: print("Eavesdropper sees:", ed[:24], "... (undecryptable ciphertext)") # They cannot read either message — no shared secret ever travelled.
Non-negotiables: each party uses only the other's public key, both messages decrypt correctly, and the eavesdropper sees only ciphertext.
Recap
3 minAsymmetric encryption uses a key pair: a public key shared freely and a private key guarded. Anyone can encrypt to you with your public key; only your private key decrypts — so no shared secret has to travel, solving the key-distribution problem. The security rests on one-way maths (e.g. factoring huge primes for RSA). Use OAEP padding, 2048-bit keys minimum, and store private keys encrypted. Because RSA is slow and size-limited, real systems use hybrid encryption: RSA encrypts a small symmetric key, AES encrypts the data — exactly how TLS, SSH, and PGP work. Next: using the keys the other way, to sign.
Vocabulary Card
- public / private key
- Shared key vs. secret key; what one encrypts, the other decrypts.
- RSA
- An asymmetric algorithm whose security rests on the hardness of factoring.
- OAEP
- The safe padding scheme for RSA encryption.
- hybrid encryption
- RSA encrypts a symmetric key; AES encrypts the bulk data.
Homework
4 minBuild a small pkcrypt.py with generate (save a PEM key pair, private key passphrase-protected), encrypt <file> <pubkey>, and decrypt <file> <privkey> using hybrid encryption. Test the full flow. Write a paragraph explaining hybrid encryption to a beginner and why HTTPS uses asymmetric crypto only to set up a symmetric key.
Sample · hybrid encryption explained
Hybrid encryption for beginners: Public-key crypto solves a hard problem — letting strangers share secrets without first agreeing on a key — but it's SLOW and can only encrypt tiny amounts. Symmetric crypto (AES) is FAST and handles any size, but both sides need the same key... which is the thing we couldn't share securely. Hybrid takes the best of both: I make up a random AES key, encrypt the big data with fast AES, then encrypt just that little AES key with your public key. I send you both. Only your private key can unwrap the AES key, and then AES unlocks the data. HTTPS does exactly this: the slow asymmetric handshake exists ONLY to agree on a symmetric session key securely; then the whole page load uses fast symmetric encryption. Asymmetric for setup, symmetric for speed.
Non-negotiables: working generate/encrypt/decrypt with hybrid + passphrase-protected private key, and a clear beginner explanation tying it to HTTPS.