Cryptography Fundamentals

Understand symmetric and asymmetric encryption, hashing, digital signatures, PKI, and TLS — the mathematical backbone of all secure communication.

Medium 65m 3 tasks

Learning Objectives

  • Differentiate symmetric and asymmetric encryption with practical examples
  • Explain hashing and why it's one-way
  • Understand digital signatures and certificate chains (PKI)
  • Describe how TLS 1.3 establishes a secure connection
  • Identify common cryptographic weaknesses: weak algorithms, misuse

Why Cryptography?

The CIA Triad depends on crypto:
- Confidentiality → encryption (AES, RSA)
- Integrity → hashing (SHA-256), HMAC
- Authentication → digital signatures, certificates

Symmetric Encryption

Same key encrypts and decrypts — fast, used for bulk data:

Plaintext → [AES-256 + Key] → Ciphertext
Ciphertext → [AES-256 + Key] → Plaintext

Common algorithms:
| Algorithm | Key Size | Status |
|-----------|----------|--------|
| AES-128/256 | 128/256 bit | ✅ Secure |
| ChaCha20 | 256 bit | ✅ Secure |
| 3DES | 112 bit effective | ⚠️ Legacy |
| DES | 56 bit | ❌ Broken |
| RC4 | variable | ❌ Broken |

from cryptography.fernet import Fernet

# Generate key
key = Fernet.generate_key()
f   = Fernet(key)

# Encrypt
plaintext  = b"sensitive data"
ciphertext = f.encrypt(plaintext)

# Decrypt
recovered  = f.decrypt(ciphertext)

Problem with symmetric: How do you securely share the key?
→ Answer: asymmetric encryption for key exchange.

Asymmetric Encryption

Two mathematically related keys:
- Public key — share with everyone (encrypts)
- Private key — keep secret (decrypts)

Alice's keys: public_alice, private_alice
Bob's keys:   public_bob, private_bob

Bob sends Alice a secret:
  secret → [encrypt with public_alice] → ciphertext
  ciphertext → [decrypt with private_alice] → secret
  Only Alice can decrypt — only she has private_alice

Common algorithms:
| Algorithm | Use | Status |
|-----------|-----|--------|
| RSA-2048/4096 | Key exchange, signatures | ✅ Secure |
| ECDSA (P-256) | Signatures | ✅ Secure |
| ECDH | Key exchange | ✅ Secure (forward secrecy) |
| DSA-1024 | Signatures | ❌ Weak |

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

# Generate key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key  = private_key.public_key()

# Encrypt with public key
ciphertext = public_key.encrypt(
    b"secret message",
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                 algorithm=hashes.SHA256(), label=None)
)

# Decrypt with private key
plaintext = private_key.decrypt(ciphertext,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                 algorithm=hashes.SHA256(), label=None)
)

Hashing

A hash function maps data of any size to a fixed-size digest:
- One-way — cannot reverse hash to input
- Deterministic — same input always gives same output
- Avalanche effect — tiny input change → completely different hash
- Collision resistant — hard to find two inputs with same hash

import hashlib

data = b"password123"
h    = hashlib.sha256(data).hexdigest()
# → "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

# SHA-256 produces 256 bits = 32 bytes = 64 hex chars
Algorithm Output Status
SHA-256 256 bit ✅ Secure
SHA-3-256 256 bit ✅ Secure
SHA-1 160 bit ❌ Broken (collision)
MD5 128 bit ❌ Broken (collision)
bcrypt variable ✅ For passwords

Password Hashing — Always Salt!

import bcrypt

# Hash password (bcrypt includes salt automatically)
password    = b"hunter2"
hashed      = bcrypt.hashpw(password, bcrypt.gensalt())

# Verify
is_valid = bcrypt.checkpw(password, hashed)

# NEVER store plain SHA-256 of passwords — use bcrypt/argon2/scrypt
# Plain SHA-256: vulnerable to rainbow table attacks
# bcrypt: slow by design (10-12 rounds), salted, work factor adjustable

HMAC – Message Authentication Code

HMAC ensures both integrity and authentication:

import hmac, hashlib

key     = b"shared_secret_key"
message = b"the data to authenticate"

mac = hmac.new(key, message, hashlib.sha256).hexdigest()
# → attach MAC to message; receiver recomputes and compares

Digital Signatures

Signatures prove who sent data and that it wasn't modified:

Signing (sender):    hash(message) → [sign with private_key] → signature
Verifying (anyone):  hash(message) → [verify with public_key, signature] → valid/invalid
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

# Sign
signature = private_key.sign(b"document content",
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256())

# Verify (raises exception if invalid)
public_key.verify(signature, b"document content",
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256())

PKI – Public Key Infrastructure

How do you know a public key really belongs to who claims?
Certificate Authorities (CA) sign certificates:

Certificate contains:
  - Subject (e.g. *.google.com)
  - Public key
  - Issuer (CA name)
  - Validity period
  - Signature by CA's private key

Chain of trust:
Root CA → Intermediate CA → Server Certificate
(in your OS/browser trust store)
# Inspect a certificate
openssl s_client -connect google.com:443 2>/dev/null | openssl x509 -text -noout

# Check certificate expiry
echo | openssl s_client -connect example.com:443 2>/dev/null     | openssl x509 -noout -dates

TLS 1.3 Handshake

Client                               Server
  │──── ClientHello ───────────────▶│  (supported ciphers, TLS 1.3, random)
  │◀─── ServerHello ────────────────│  (chosen cipher, server random)
  │◀─── Certificate ────────────────│  (server's certificate)
  │◀─── CertificateVerify ──────────│  (proof server has private key)
  │◀─── Finished ───────────────────│  (HMAC of handshake)
  │──── Finished ───────────────────▶│
  │════ Encrypted Application Data ═│

Key exchange uses ECDHE → forward secrecy (past sessions safe if key later compromised).

Common Cryptographic Weaknesses

Weakness Example Risk
Weak algorithm MD5, DES, RC4 Broken, collision
Short key RSA-512, DES Brute-forceable
ECB mode AES-ECB for blocks Pattern leakage
Hardcoded key key = "mysecretkey" Key exposure
No salt SHA256(password) Rainbow table
Predictable IV static IV in CBC Duplicate encryption
Timing attack string comparison Leak key bits

Using Python hashlib: (1) compute SHA-256 of 'password', 'password123', 'Password123!', (2) observe the avalanche effect — change one character and show the new hash, (3) use hashcat (or online tools) to crack the MD5 hash '5f4dcc3b5aa765d61d8327deb882cf99', (4) compare bcrypt and SHA-256 timing for 10,000 hash operations.

✦ Answer the questions to complete this task

Why is bcrypt preferred over SHA-256 for password storage?

What is a rainbow table attack?

Using openssl: (1) connect to github.com:443 and dump the certificate, (2) identify: subject, issuer, validity dates, public key algorithm, (3) check if Perfect Forward Secrecy is used (look for ECDHE in cipher), (4) test a site with expired or self-signed cert.

✦ Answer the questions to complete this task

What does Perfect Forward Secrecy mean?

Write a Python function that: (1) takes a message and shared secret, computes HMAC-SHA256, (2) attaches it to the message as a signature, (3) implements a verify() function that rejects tampered messages, (4) demonstrates that changing one byte in the message invalidates the MAC. Use hmac.compare_digest() to prevent timing attacks.

✦ Answer the questions to complete this task

Why use hmac.compare_digest() instead of == for MAC comparison?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

Cryptography Fundamentals MCQ

Cryptography Fundamentals MCQ

Start →
⚙️ Practical Medium +30 XP

Build an Encrypted Message System

Build an Encrypted Message System

Start →
🚩 Challenge Hard +50 XP

Break the Weak Encryption

Break the Weak Encryption

Start →