Password Attacks & Cryptography · beginner · ~12 min

Symmetric and asymmetric encryption

**What you will learn** - Define symmetric and asymmetric encryption and explain the core difference: one shared key versus a public/private key pair. - Choose the right tool for a job: AES-GCM for bulk data, public-key crypto for key exchange and signatures. - Explain why **ECB mode** leaks patterns and why **authenticated encryption** (AES-GCM) is the safe default. - Describe the **key-distribution problem** and how asymmetric crypto solves it. - Trace how a **hybrid** scheme (the basis of TLS) combines both families, and recognize common misuse in real code and config. - Apply defensive habits: never roll your own crypto, use vetted libraries, manage keys and IVs/nonces correctly, and verify a fix by testing that tampering is rejected.

Overview

Security objective. The asset you are protecting in this lesson is confidential data — a message, a file, a database column, or a network stream — plus the keys that guard it. The threat is an attacker who can read and modify whatever travels over the network or sits in storage. By the end you will be able to prevent that attacker from reading the data (confidentiality) and detect any attempt to tamper with it (integrity), and you will be able to tell when a system uses the wrong tool for the job.

Encryption is how we keep data confidential when someone untrusted might see it — on a network, on a stolen laptop, or in a database backup. Unlike hashing (covered in the prerequisite lesson Hashing: integrity, identification, and its limits), encryption is reversible, but only if you hold the right key. A hash is a one-way fingerprint you cannot undo; encryption is a locked box you can reopen. That difference — the key — is the whole point. Hashing gave you integrity and identification; encryption adds confidentiality, and (through signatures) a stronger, attributable form of integrity.

There are two families of encryption, and almost every secure system you use relies on both at once.

Symmetric encryption uses a single shared secret key for both locking (encrypt) and unlocking (decrypt). It is fast and ideal for large amounts of data — file contents, a network stream, a disk volume.

Asymmetric encryption (also called public-key cryptography) uses a pair of mathematically linked keys: a public key you can hand out to anyone, and a private key you guard. What one key locks, only the other can unlock. This solves a problem symmetric crypto cannot: how do two strangers agree on a secret key without an eavesdropper learning it? It also enables digital signatures, which prove who sent a message.

In practice these two are not competitors — they are partners. Real systems are hybrid: they use slow asymmetric crypto briefly to exchange a fast symmetric key, then use the symmetric key for the actual data. This is exactly how TLS (the next lesson, TLS, certificates, and digital signatures) protects your web traffic.

With the vocabulary in place — plaintext, ciphertext, key, cipher, mode of operation, IV/nonce, signature — we can look at each family in depth.

Why it matters

Cryptography is the backbone of every trustworthy system, and misusing it is one of the most common, most damaging mistakes in real software. The failures are rarely "the math was broken" — they are "the developer picked the wrong tool or used the right tool wrong."

  • Confidentiality of data at rest and in transit. Symmetric encryption protects database fields, backups, disk volumes, and TLS sessions. Choosing ECB instead of GCM, or reusing a nonce, silently destroys that protection while everything still appears to work — the code runs, the tests pass, and the data is exposed anyway.
  • Trust on an open network. Asymmetric crypto lets your browser confirm it is really talking to your bank and not an impostor, and lets two parties that have never met derive a shared secret. Without it, the internet has no foundation for trust.
  • Authenticity and non-repudiation. Digital signatures prove a software update, a JWT, or a financial message came from who it claims and was not altered. Understanding signatures is what lets you reason about JWT validation, code signing, and certificate chains. (Remember: decoding a JWT is not verifying its signature — that distinction comes straight from this lesson.)
  • Spotting misuse in review. Once you can tell these families apart, you can flag the classics in an authorized code review: unauthenticated encryption (no integrity check), ECB mode, hardcoded keys, reused nonces, and "encryption" used where a signature was actually needed. Being able to name the defect and propose the fix is what turns a finding into a remediation.

Core concepts

Each idea below builds on the last. Read them in order.

1. Plaintext, ciphertext, key, and cipher

Definition. Plaintext is the readable data. A cipher (algorithm) plus a key (secret) transforms it into ciphertext (scrambled). Decryption reverses the process with the appropriate key.

Why it works. A good cipher is designed so that without the key, ciphertext is computationally indistinguishable from random noise. The secrecy lives in the key, not the algorithm — this is Kerckhoffs's principle. AES is public and studied by the whole world; its strength comes from the key.

  plaintext  --[ cipher + KEY ]-->  ciphertext
  "transfer $100"                    8f3a1c...d92   (looks like noise)

  ciphertext --[ cipher + KEY ]-->  plaintext

When / when not. Use encryption when you need to recover the data later (confidentiality). Do not use it when you only need a one-way fingerprint — that is hashing.

Pitfall. "Security through obscurity" — hiding a homemade algorithm and hoping nobody figures it out. Vetted algorithms with secret keys win every time.

Knowledge check: In your own words, what single piece of information must stay secret for AES to be safe — the algorithm, or the key?

2. Symmetric encryption

Definition. One shared secret key encrypts and decrypts. The same key on both ends.

How it works internally. Modern symmetric ciphers like AES are block ciphers: they transform fixed-size blocks (AES = 16 bytes) using the key. To encrypt data longer than one block, a mode of operation decides how blocks chain together. The mode is at least as important as the cipher.

When to use it. Bulk data: file/disk encryption, the body of a TLS session, encrypting a database column. It is fast — hardware AES runs at gigabytes per second.

When NOT to use it directly. When two parties have no pre-shared secret and need to agree on one over an untrusted channel (that is asymmetric's job), or when you need to prove who produced the data (use a signature).

The two big rules:

  • Use an authenticated modeAES-GCM is the standard choice. "Authenticated" means it provides confidentiality and integrity: if anyone flips a bit of the ciphertext, decryption fails instead of returning garbage. Plain CBC or CTR alone give you confidentiality but no tamper detection.
  • Never use ECB. ECB encrypts each block independently, so identical plaintext blocks map to identical ciphertext blocks — leaking structure. The famous "ECB penguin" image is still clearly recognizable after ECB encryption.
ECB (BAD) — identical input blocks -> identical output blocks
  [block A][block A][block B]  -->  [X][X][Y]   pattern leaks!

GCM (GOOD) — each block depends on a unique nonce + position
  [block A][block A][block B] + nonce  -->  [P][Q][R]   no visible pattern
                                              + auth tag (detects tampering)

Pitfall. Reusing a nonce/IV with the same key in GCM is catastrophic — it can leak plaintext and forge the authentication tag. Each encryption needs a fresh, unique nonce.

Knowledge check (predict the output): You encrypt the message AAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAA (two identical 16-byte blocks) with AES-ECB. What do the two ciphertext blocks look like relative to each other, and why is that a problem?

3. Asymmetric (public-key) encryption

Definition. A key pair: a public key (shareable) and a private key (secret). They are mathematically linked so that what one encrypts, only the other can decrypt.

How it works internally. It relies on "trapdoor" math — operations easy to do one way and infeasible to reverse without the private key. RSA rests on the difficulty of factoring huge numbers; ECC (elliptic-curve cryptography) rests on the elliptic-curve discrete-log problem and gives equivalent security with much smaller keys.

Two distinct operations the pair enables:

  • Encrypt to someone: anyone encrypts with your public key; only your private key decrypts. This solves key distribution — the public key can travel over an open channel.
  • Sign: you sign a hash of your message with your private key; anyone verifies with your public key. A valid signature proves the message came from the private-key holder and was not altered. (This is why the Hashing prerequisite matters — signatures sign a hash, not the whole message. And note: verifying a signature is a real cryptographic check — merely reading or decoding a token is not verification.)
CONFIDENTIALITY (encrypt to a recipient)
  sender  --[ encrypt with RECIPIENT's PUBLIC key ]-->  ciphertext
  recipient --[ decrypt with their PRIVATE key ]-->  plaintext

AUTHENTICITY (sign)
  signer  --[ sign hash with OWN PRIVATE key ]-->  signature
  verifier --[ verify with signer's PUBLIC key ]-->  valid? yes/no

When to use it. Key exchange, digital signatures, certificates. When NOT to: encrypting large data directly — it is slow and size-limited. Use it to protect a symmetric key instead.

Pitfall. Confusing the operations: encrypting with your private key does not give confidentiality (anyone has your public key and can decrypt). Public key = confidentiality to you; private key = signatures from you.

Knowledge check: You want to send a file so that only Alice can read it. Whose key do you use, and is it her public or private key?

4. The key-distribution problem

Definition. The challenge of getting a shared secret key to the other party without an eavesdropper intercepting it.

Why it is hard. With symmetric crypto alone, sharing the key is the same problem as sharing the secret data — circular. Asymmetric crypto breaks the circle: publish a public key openly; only the matching private key can use it. Modern systems often go further with key-exchange protocols (Diffie-Hellman / ECDHE) that let both sides derive a shared secret without ever transmitting it, which also gives forward secrecy.

5. Hybrid encryption (the real world)

Definition. Combine both families: asymmetric crypto to authenticate parties and establish a symmetric key, then fast symmetric crypto for the data.

  1. Asymmetric step (slow, brief):
        agree on / deliver a random SESSION KEY (symmetric)
  2. Symmetric step (fast, bulk):
        encrypt all the actual data with AES-GCM using the session key

When to use it. Essentially always for real communication — it is how TLS/HTTPS, SSH, and encrypted messaging work. Pitfall: assuming TLS is "all RSA" or "all AES" — it is the combination that makes it both secure and fast.

Knowledge check (explain in your own words): Why don't real systems just use asymmetric encryption for everything, given that it solves key distribution?

6. The threat model for an encryption boundary

Before writing any crypto, name what you are protecting and from whom.

  ASSETS:        plaintext data; the symmetric session key; the private key
  ENTRY POINTS:  the network or storage carrying ciphertext
                 (an attacker can READ and MODIFY it)
  TRUST BOUNDARY:
      [ your process | keys in a KMS ]  <=== untrusted channel ===>  [ recipient ]
         trusted, key material lives here            attacker sits on this wire
  ATTACKER CAN:      observe ciphertext, replay it, flip bits, reorder messages
  ATTACKER MUST NOT: read plaintext, forge a valid auth tag, or learn any key

Knowledge check: In this diagram, where is the trust boundary, and which insecure assumption — "the channel is private" or "nobody will modify the ciphertext" — does authenticated encryption (GCM) specifically defend against? Which log event (hint: a decrypt outcome) would let you detect an attacker flipping bits? And why must any experiment that exercises this be run only in an authorized, isolated lab?

Syntax notes

This is a concept lesson, so the "syntax" is the shape of a correct authenticated-encryption call rather than language grammar. Real code uses a vetted library; the structure is always: a key, a unique nonce/IV, the plaintext, optional associated data, producing ciphertext + an authentication tag.

# AES-GCM, the recommended authenticated symmetric scheme (pseudocode shape, lab-safe)

key        = 32 random bytes        # 256-bit key, kept secret, stored in a key store
nonce      = 12 random bytes        # UNIQUE per encryption; never reused with this key
ciphertext, tag = AES_GCM_encrypt(key, nonce, plaintext, aad)
#                                                          ^ associated data:
#                                                            authenticated but not encrypted

# To decrypt, you need the SAME key + nonce; the tag is verified first.
plaintext  = AES_GCM_decrypt(key, nonce, ciphertext, tag, aad)   # FAILS if tampered

Key points: the nonce is not secret (it ships alongside the ciphertext) but must be unique; the tag is what makes the scheme authenticated; never write the encryption primitive yourself. In example config or code, use placeholders such as API_KEY=<development-placeholder> — never a real key.

Lesson

Encryption is reversible — but only with a key. That key is what makes it different from hashing, and it is what provides confidentiality.

Symmetric encryption

In symmetric encryption, a single shared secret key is used to both encrypt and decrypt. It is fast, so it is the right choice for bulk data.

Key points:

  • AES is the standard algorithm.
  • Use an authenticated mode such as AES-GCM. Authenticated means it protects both confidentiality and integrity (it detects tampering).
  • Avoid ECB mode. ECB encrypts each block independently, so identical plaintext blocks produce identical ciphertext blocks. This leaks patterns — the famous "ECB penguin" image is still clearly visible after ECB encryption.

The main challenge is key distribution: how do two parties share the secret key safely in the first place?

Asymmetric (public-key) encryption

Asymmetric encryption uses a key pair:

  • A public key, which you can share freely.
  • A private key, which you keep secret.

What one key encrypts, only the other can reverse. This enables two distinct operations:

  • Encrypt to someone: use their public key. Only their private key can decrypt. This solves the key-distribution problem.
  • Sign: encrypt a hash of your message with your private key. Anyone can verify it with your public key. This proves authenticity and integrity — a digital signature.

RSA and elliptic-curve cryptography (ECC) are the common families. Asymmetric crypto is slower, so it is used to exchange a symmetric key, not to encrypt bulk data.

The hybrid reality (TLS)

Real systems combine both approaches:

  1. Use asymmetric crypto to authenticate the parties and exchange a key.
  2. Use fast symmetric crypto for the actual data.

This is exactly how TLS works (the topic of the next lesson).

Code examples

The example below follows the insecure → secure → verify shape. All of it runs on your own machine only; no network target is involved. It uses Python's well-vetted cryptography library, because crypto should always go through a reviewed library rather than hand-written primitives. Install it in a throwaway virtualenv with pip install cryptography.

1. WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

This shows why ECB is unsafe: identical plaintext blocks become identical ciphertext blocks, and there is no integrity tag at all.

# WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab.
# Do not deploy. ECB leaks structure and provides NO tamper detection.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import os

key = os.urandom(32)
# Two identical 16-byte blocks of plaintext:
plaintext = b"YELLOW_SUBMARINE" + b"YELLOW_SUBMARINE"

enc = Cipher(algorithms.AES(key), modes.ECB()).encryptor()   # ECB: the mistake
ct = enc.update(plaintext) + enc.finalize()

print("block 1:", ct[:16].hex())
print("block 2:", ct[16:].hex())
print("blocks identical?", ct[:16] == ct[16:])   # True -> pattern leaked!

The two ciphertext blocks are byte-for-byte identical, so an observer learns that the two plaintext blocks were the same — structure has leaked, and nothing detects tampering.

2. SECURE fix — hybrid encryption (RSA-OAEP wraps an AES-256-GCM session key)

This mirrors how TLS combines the two families: RSA protects a one-time symmetric key; AES-GCM protects the data and authenticates it.

# Hybrid encryption: RSA (asymmetric) wraps an AES-256-GCM (symmetric) session key.
import os
from cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# --- 1. Recipient generates a key pair (private stays secret; public is shareable) ---
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# --- 2. Sender creates a fresh symmetric session key and a UNIQUE nonce ---
session_key = AESGCM.generate_key(bit_length=256)   # 32-byte symmetric key
nonce = os.urandom(12)                               # 96-bit nonce, never reused with this key
aesgcm = AESGCM(session_key)

message = b"transfer 100 to account 4815"
aad = b"context:payment-v1"    # authenticated but not encrypted (binds context)

# --- 3. Symmetric step: encrypt the bulk data (fast); output includes the auth tag ---
ciphertext = aesgcm.encrypt(nonce, message, aad)    # tag appended automatically

# --- 4. Asymmetric step: encrypt the small session key with the recipient's PUBLIC key ---
wrapped_key = public_key.encrypt(
    session_key,
    asym_padding.OAEP(                               # OAEP padding: never use textbook/raw RSA
        mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None,
    ),
)

# ===== Transmit: wrapped_key + nonce + ciphertext + aad. None are secret except the
#       recipient's private key, which never left their machine. =====

# --- 5. Recipient decrypts the session key with their PRIVATE key, then the data ---
recovered_key = private_key.decrypt(
    wrapped_key,
    asym_padding.OAEP(
        mgf=asym_padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None,
    ),
)
plaintext = AESGCM(recovered_key).decrypt(nonce, ciphertext, aad)   # raises if tampered
print("Decrypted:", plaintext.decode())

Expected output:

Decrypted: transfer 100 to account 4815

3. VERIFY — prove the fix rejects bad input and accepts good input

A fix you have not tested is a hope, not a control. These checks confirm both directions.

# Reuses aesgcm, nonce, aad, message, ciphertext from step 2.
from cryptography.exceptions import InvalidTag

# (a) ACCEPTS good input: untouched ciphertext decrypts back to the original.
assert aesgcm.decrypt(nonce, ciphertext, aad) == message
print("good input accepted: OK")

# (b) REJECTS a flipped ciphertext byte.
tampered = bytearray(ciphertext); tampered[0] ^= 0x01
try:
    aesgcm.decrypt(nonce, bytes(tampered), aad)
    print("FAIL: tampered ciphertext was accepted")
except InvalidTag:
    print("tampered ciphertext rejected: OK")

# (c) REJECTS altered associated data.
try:
    aesgcm.decrypt(nonce, ciphertext, b"context:payment-v2")
    print("FAIL: altered AAD was accepted")
except InvalidTag:
    print("altered AAD rejected: OK")

# (d) Confirms we are NOT in ECB: two encryptions of the same plaintext differ,
#     because each uses a fresh nonce.
ct1 = aesgcm.encrypt(os.urandom(12), message, aad)
ct2 = aesgcm.encrypt(os.urandom(12), message, aad)
assert ct1 != ct2
print("same plaintext -> different ciphertext: OK")

Expected output:

good input accepted: OK
tampered ciphertext rejected: OK
altered AAD rejected: OK
same plaintext -> different ciphertext: OK

What it proves. GCM raises InvalidTag on any change to ciphertext or AAD (integrity works), it still returns the original for untouched input (correctness), and the fresh-nonce design means identical plaintext never produces identical ciphertext (no ECB leak). Note also that RSA can only encrypt data smaller than its key size — which is exactly why we wrap a 32-byte key, not the whole message.

Lab cleanup. Delete the virtualenv (rm -rf venv) and any in-memory key variables when done. Because keys were generated fresh in the process and never written to disk, nothing persists after the interpreter exits.

Line by line

A walkthrough of the secure hybrid example (step 2), in execution order.

Step Code What happens / state
1 rsa.generate_private_key(...) Creates a 2048-bit RSA key pair. The private key must stay on the recipient's machine; the public key can be published anywhere.
2 AESGCM.generate_key(256) Produces a random 32-byte session key — the symmetric secret used only for this one message.
3 os.urandom(12) Generates a fresh 12-byte nonce. Unique-per-message is mandatory; it is sent in the clear.
4 aesgcm.encrypt(nonce, message, aad) The symmetric step: scrambles the message and appends an authentication tag. aad ("payment-v1") is authenticated but left readable, binding the ciphertext to its context. ciphertext now looks like random bytes.
5 public_key.encrypt(session_key, OAEP...) The asymmetric step: encrypts the small session key with the recipient's public key. Result wrapped_key can be decrypted only by the matching private key.
6 (transmit) wrapped_key + nonce + ciphertext + aad travel together. None reveal the message; only the recipient's private key can unlock the chain.
7 private_key.decrypt(wrapped_key, OAEP...) Recipient recovers the exact 32-byte session key. recovered_key == session_key.
8 AESGCM(recovered_key).decrypt(...) GCM first verifies the tag. If valid, it returns the original plaintext; if anything was altered, it raises InvalidTag.

The key insight: the slow public-key operation runs once on 32 bytes; the fast symmetric operation handles all the bulk data. That division of labor is the entire reason hybrid schemes exist. In the verify step, note how (b) and (c) exercise the failure path deliberately — you have not proven a security control until you have seen it say "no."

Common mistakes

Realistic errors learners (and shipping developers) make.

Mistake 1 — Using ECB mode "because it was the default example."

# WRONG
cipher = AES.new(key, AES.MODE_ECB)   # identical blocks -> identical ciphertext

Why it is wrong: ECB leaks structure; repeated plaintext blocks are visible in the ciphertext (the ECB-penguin effect), and it provides no integrity.

# CORRECT
aesgcm = AESGCM(key)
ct = aesgcm.encrypt(unique_nonce, plaintext, aad)   # authenticated, no pattern leak

Recognize/prevent it: if you see MODE_ECB anywhere, treat it as a bug; default to AES-GCM. Verify by encrypting two identical blocks and confirming the ciphertext blocks differ.

Mistake 2 — Reusing a nonce/IV with the same key. Why it is wrong: GCM's security collapses if a (key, nonce) pair repeats — it can reveal plaintext relationships and let attackers forge the auth tag. Prevent it: generate a fresh random nonce per message, or use a strictly increasing counter you can prove never repeats. Verify by asserting two encryptions of the same plaintext produce different ciphertext.

Mistake 3 — Encrypting without authenticating (no integrity).

# WRONG: confidentiality only, attacker can flip bits undetected
ct = aes_cbc_encrypt(key, iv, plaintext)

Why it is wrong: an attacker can tamper with ciphertext and you will decrypt corrupted (or attacker-chosen) data without noticing — the basis of padding-oracle and bit-flipping attacks. Correct: use an AEAD mode like GCM, which produces and verifies a tag. Verify with the flip-a-byte test that expects InvalidTag.

Mistake 4 — Confusing encryption with signing. Learners sometimes "encrypt with the private key" to prove identity. Anyone holding the public key can decrypt it, so it gives no confidentiality. If you want authenticity, use a real signature API (RSA-PSS, Ed25519); if you want confidentiality to a recipient, encrypt with their public key. Related misconception: decoding a JWT is not verifying its signature — decoding just base64-unpacks the token; verification runs a cryptographic check against a key.

Mistake 5 — Hardcoding keys in source code.

# WRONG
KEY = b"hardcoded-secret-key-1234567890ab"

Why it is wrong: the key ends up in version control, logs, and binaries. Correct: load keys from a secrets manager / environment / KMS, and rotate them. Use placeholders like API_KEY=<development-placeholder> in examples, never real secrets.

Mistake 6 — Rolling your own crypto. Hand-written ciphers, custom XOR "encryption," or homemade padding are nearly always broken. Use audited libraries (libsodium, OpenSSL via a high-level wrapper, Python cryptography).

Mistake 7 — "The scanner passed, so it's secure." A clean automated scan means the scanner found nothing it knows to look for — not that the design is sound. Nonce reuse, wrong key roles, and missing key rotation routinely slip past scanners. Nothing is ever "completely secure"; reason about the threat model, not the tool's green checkmark.

Debugging tips

Common library / setup errors

  • ModuleNotFoundError: cryptography → the library is not installed; run pip install cryptography in the right environment/virtualenv.
  • ValueError: Data too large for key size (RSA) → you tried to RSA-encrypt data bigger than the key allows. Fix: wrap a small symmetric key, not the whole message (the hybrid pattern).

Common runtime errors

  • InvalidTag / authentication-failed on decrypt → the ciphertext, nonce, tag, key, or aad does not match what was used to encrypt. This is usually correct behavior signaling tampering or a mismatch — check that all of them travel together unchanged and that aad is byte-for-byte identical.
  • Decryption returns garbage with no error → you are almost certainly using an unauthenticated mode (CBC/CTR/ECB). Switch to GCM so mismatches fail loudly.

Common logic errors

  • Same nonce on every call → look for a constant nonce/IV; generate a fresh one per encryption.
  • Wrong key direction → for confidentiality you encrypt with the recipient's public key and decrypt with their private key; getting this backwards "works" only in the sense that it runs, but provides no secrecy.
  • Encoding mismatches → encrypting str vs bytes, or base64-decoding inconsistently between sender and receiver, corrupts the ciphertext. Keep everything as bytes end to end.

Questions to ask when it doesn't work

  1. Do the key, nonce, tag, and AAD on the decrypt side exactly equal what I produced on encrypt?
  2. Am I using an authenticated mode (GCM), so failures surface as exceptions?
  3. Is the nonce unique for this key?
  4. Am I confusing the public and private key roles for what I'm trying to achieve (secrecy vs. signature)?

Memory safety

Security & safety

Authorization and ethics: All crypto experimentation here is defensive and must run only on systems you own or in an authorized, isolated lab (localhost, containers, a CTF/training VM). Before running any lab, confirm: (1) you own the machine or have written authorization, (2) it is isolated from production and third-party networks, (3) no real user data or real secrets are involved, and (4) you have a cleanup/reset step. Never use these techniques against third-party systems, and never embed real secrets, keys, IPs, or hostnames — use placeholders such as API_KEY=<development-placeholder>.

Threat model for an encryption boundary

  ASSETS:        plaintext data, the symmetric session key, the private key
  ENTRY POINTS:  the network/storage carrying ciphertext (attacker can read & modify)
  TRUST BOUNDARY:
      [ your process | keys in KMS ]  <===== untrusted channel =====>  [ recipient ]
  ATTACKER CAN:  observe ciphertext, replay it, flip bits, reorder messages
  ATTACKER MUST NOT: read plaintext, forge a valid tag, learn any key

Defensive practices (the priorities)

  • Use authenticated encryption (AEAD/GCM) so tampering is detected, not silently decrypted.
  • Never reuse a (key, nonce) pair. Generate nonces with a CSPRNG (os.urandom) or a proven counter.
  • Protect keys, not algorithms. Store keys in a secrets manager or KMS, restrict access with least privilege, and rotate them. The private key must never leave its trust boundary.
  • Prefer well-reviewed libraries (libsodium, the cryptography package, OpenSSL via a vetted wrapper). Do not implement primitives yourself.
  • Use safe padding for RSA (OAEP for encryption, PSS for signatures) — never textbook/raw RSA or PKCS#1 v1.5 for new designs.
  • Compare secrets in constant time (see the related exercise constant-time-compare): comparing tags, MACs, or tokens with a short-circuiting == leaks timing information an attacker can exploit.

Detection & logging

  • What to log: timestamp, source identifier, the resource/operation (e.g., "decrypt session X"), the result (success/InvalidTag), the security decision made, and a correlation id to tie related events together. A spike in InvalidTag failures from one source can indicate tampering or an attack; key-access and key-rotation events belong in the log too.
  • What to NEVER log: encryption keys, private keys, plaintext, nonces tied to a specific key, passwords, session tokens/cookies, or unneeded PII. Log identifiers and outcomes, not the sensitive values.
  • False positives: legitimate causes — a truncated/corrupted network packet, mismatched library versions, or an out-of-date client sending old AAD — can also raise InvalidTag. Correlate across events before treating a single failure as an attack.

Verifying a fix works (mitigation verification)

  • Test that flipping any byte of ciphertext, nonce, tag, or AAD makes decryption raise an error (confirms integrity protection is active).
  • Test that untouched ciphertext still decrypts to the original (confirms you did not break the good path).
  • Test that two encryptions of the same plaintext produce different ciphertext (confirms a fresh nonce and that you are not in ECB).

Real-world uses

Concrete authorized uses

  • HTTPS / TLS (the next lesson): the browser and server use asymmetric crypto and certificates to authenticate and agree on a symmetric session key, then AES-GCM protects every byte of page data. Hybrid encryption in action.
  • Encrypted messaging (Signal, WhatsApp): public-key key agreement establishes per-conversation symmetric keys; messages themselves are symmetrically encrypted.
  • Disk and database encryption: AES (symmetric) protects volumes (BitLocker, LUKS, FileVault) and at-rest database fields, because it is fast on large data.
  • Code signing and software updates: a vendor signs releases with a private key; your device verifies with the public key, proving authenticity before installing.
  • JWTs and APIs: tokens are signed (asymmetric or HMAC) so a server can verify — not merely decode — that they were issued by a trusted party and not altered.
  • SSH: public-key authentication lets you log in without sending a password, then a symmetric cipher protects the session.

Professional best practices

Beginner rules:

  • Default to AES-GCM for symmetric encryption; never ECB.
  • Use a fresh, unique nonce/IV per encryption.
  • Use audited libraries; never write your own cipher.
  • Keep keys out of source code; load them from configuration or a secrets store.
  • Validate inputs as bytes and handle decrypt failures explicitly (catch InvalidTag, log the outcome, fail closed).

Advanced habits:

  • Plan key management and rotation from day one (KMS, envelope encryption, key versioning so old data still decrypts).
  • Use the right primitive for the goal: AEAD for confidentiality+integrity, signatures (RSA-PSS / Ed25519) for authenticity, ECDHE for forward secrecy in key exchange.
  • Prefer modern, smaller-key ECC (e.g., Ed25519/X25519) over large RSA where supported.
  • Use associated data (AAD) to bind ciphertext to its context and prevent confused-deputy/replay misuse.
  • Compare secrets in constant time and minimize how long plaintext keys live in memory.
  • Apply least privilege to key access and enable audit logging on the key store — with the logging rules above (outcomes and identifiers, never the key material).

Practice tasks

Beginner 1 — Classify the scenario. Objective: cement the symmetric-vs-asymmetric distinction. For each scenario, state which family fits and why: (a) encrypting a 4 GB backup file, (b) two strangers agreeing on a secret over the open internet, (c) proving a software update came from the vendor, (d) encrypting one column in a database. Constraint: one sentence of justification each. Hint: think "speed/bulk" vs "key distribution / authenticity." Concepts: symmetric, asymmetric, signatures, hybrid.

Beginner 2 — Spot the ECB pattern. Objective: see why ECB leaks structure. Take a message made of three identical 16-byte blocks followed by one different block, and describe (in writing or with a diagram) what the ECB ciphertext block pattern would be, then what a GCM ciphertext pattern would be. Expected description: ECB → [X][X][X][Y]; GCM → four unrelated-looking blocks plus a tag. Hint: ECB encrypts each block independently. Concepts: block cipher, mode of operation, ECB pitfall.

Intermediate 1 — Build a round-trip with AES-GCM (lab-only). Objective: encrypt and decrypt a message using a vetted library's AEAD API. Requirements: generate a 256-bit key and a fresh 12-byte nonce, encrypt a string, then decrypt it back to the original. Input/output example: input "hello lab" → ciphertext bytes → decrypted "hello lab". Constraints: run on localhost only; do not hardcode the key in committed code; use a fresh nonce. Hint: keep everything as bytes; ship the nonce alongside the ciphertext. Defensive conclusion: confirm the round-trip succeeds, then delete the key variable / virtualenv. Concepts: symmetric encryption, nonce, authenticated encryption.

Intermediate 2 — Tamper-detection test (lab-only). Objective: prove your encryption is authenticated. Requirements: after encrypting with AES-GCM, flip one byte of the ciphertext and attempt to decrypt; confirm it raises an error rather than returning data. Then flip a byte of the AAD and observe the same. Expected output: an InvalidTag/authentication error in both cases. Constraint: run only in your local lab. Hint: this is the behavior that distinguishes AEAD from plain CBC. Defensive conclusion: this test is your mitigation-verification step — record that tampering is rejected, and note that in production you would log each such failure (outcome + source, never the key). Concepts: integrity, AEAD, AAD, detection.

Challenge — Implement and harden hybrid encryption end to end (lab-only). Objective: replicate the lesson's hybrid pattern yourself, then verify it. Requirements: generate an RSA (or X25519) key pair; create a random AES-256-GCM session key; encrypt a multi-kilobyte message with the session key; wrap the session key with the recipient's public key using OAEP padding; then, as the recipient, unwrap the key and decrypt. Add a tamper test that corrupts the wrapped key and shows decryption fails, and a good-path test that shows an untouched message decrypts correctly. Constraints: use a reviewed crypto library, OAEP (not raw RSA), a unique nonce, no hardcoded secrets, and run entirely on your own machine. Hint: only the small session key goes through RSA; all bulk data goes through GCM. Defensive conclusion: write down (1) which log events you would emit for decrypt failures and key access, (2) that you never log the keys or plaintext, and (3) how you would rotate the key pair. Then reset the lab. Concepts: hybrid encryption, key wrapping, RSA-OAEP, AES-GCM, threat model, detection/logging.

Summary

  • Encryption is reversible with a key — that is what separates it from hashing and what provides confidentiality (Kerckhoffs's principle: secrecy lives in the key, not the algorithm).
  • Symmetric uses one shared key, is fast, and handles bulk data. Use AES-GCM (authenticated: confidentiality + integrity); never ECB, and never reuse a (key, nonce) pair.
  • Asymmetric uses a public/private key pair. Encrypt to someone with their public key (confidentiality); sign with your private key (authenticity — and verifying a signature is a real check, not just decoding). RSA and ECC are the common families; it is slow, so it is used on small data like keys.
  • It solves the key-distribution problem that symmetric crypto alone cannot; hybrid schemes combine both (asymmetric to exchange a symmetric session key, symmetric for the data) — the foundation of TLS, covered next.
  • Verify every fix: untouched data decrypts, tampered data raises InvalidTag, and identical plaintext yields different ciphertext. Log decrypt outcomes and key access (never the keys or plaintext), and remember no automated scan proves a system "completely secure."
  • Most common mistakes: ECB mode, nonce reuse, unauthenticated encryption, confusing encrypt-vs-sign, hardcoded keys, and rolling your own crypto. Defend by using vetted libraries, AEAD, proper key management, least privilege, constant-time comparison, and failure logging that never records secrets — all tested only in an authorized, isolated lab.

Practice with these exercises