DELIGHT Cybersecurity Workbook Series · Encryption Series
Four self-guided, hands-on exercises covering asymmetric encryption, TLS analysis, password security, and authenticated encryption.
Open a terminal on your Kali Linux machine and run the commands below before starting any lab. On Kali, most tools are already present; the commands below confirm versions and install anything missing.
# 1. Confirm you are on Kali (optional check) cat /etc/os-release | grep PRETTY_NAME # 2. Update package lists sudo apt update # 3. Install / confirm all required tools in one shot sudo apt install -y openssl wireshark hashcat john python3-pip # 4. rockyou.txt is shipped compressed on Kali — unzip it sudo gunzip /usr/share/wordlists/rockyou.txt.gz # If already unzipped you will see "not in gzip format" — that is fine. # 5. Install Python libraries used in Labs 1, 3 and 4 pip3 install cryptography bcrypt argon2-cffi # 6. Allow Wireshark captures without root (Lab 2) sudo usermod -aG wireshark $USER newgrp wireshark # 7. Create a dedicated working directory for all labs mkdir -p ~/crypto_labs && cd ~/crypto_labs echo "Working directory: $(pwd)"
Every lab in this workbook assumes your terminal is open inside ~/crypto_labs. Keep it as your working directory throughout.
All commands in this lab run directly in your Kali terminal. OpenSSL is pre-installed on Kali — no setup needed. Navigate to your working directory first, then follow each step in order.
# Make sure you are in the lab directory cd ~/crypto_labs # Confirm OpenSSL is available openssl version # Expected: OpenSSL 3.x.x ... # For the Python bonus steps, confirm the library python3 -c "from cryptography.hazmat.primitives.asymmetric import rsa; print('OK')"
Core concept — asymmetric cryptography
RSA uses a mathematically linked pair of keys. The public key encrypts data; only the paired private key can decrypt it. The same pairing is used in reverse for signatures: you sign with the private key and anyone can verify with the public key.
Create a 2048-bit private key, then extract the public key from it. Run these commands directly in your terminal from ~/crypto_labs.
# Generate a 2048-bit private key — output saved to my_private.pem openssl genrsa -out my_private.pem 2048 # Extract the public key from it openssl rsa -in my_private.pem -pubout -out my_public.pem # Verify both files exist ls -lh my_private.pem my_public.pem # Inspect the key structure (optional — press q to exit) openssl rsa -in my_private.pem -text -noout | head -20
-text output format, it is cosmetic only — the keys are valid.
Share your my_public.pem with a classmate. Save their key as partner_public.pem in the same directory. If working alone, simulate a second participant:
# Simulate a second user's key pair in the same directory openssl genrsa -out partner_private.pem 2048 openssl rsa -in partner_private.pem -pubout -out partner_public.pem # Confirm you now have four .pem files ls *.pem
Write a plaintext message and encrypt it so only the holder of partner_private.pem can read it.
# Create a short plaintext file echo "Meet me at the clock tower at noon." > message.txt # Encrypt using the partner's public key (OAEP padding) openssl pkeyutl -encrypt \ -pubin -inkey partner_public.pem \ -in message.txt \ -out message.enc # Confirm the output is unreadable binary xxd message.enc | head -4 # You should see hex bytes — no readable text
Use the partner's private key to recover the original message. In a real exchange your partner would do this with their private key on their machine.
# Decrypt using partner's private key openssl pkeyutl -decrypt \ -inkey partner_private.pem \ -in message.enc \ -out message_decrypted.txt # Read the recovered plaintext cat message_decrypted.txt # Expected: Meet me at the clock tower at noon.
Digital signatures prove authenticity and integrity. OpenSSL hashes the document and encrypts that hash with your private key. Then tamper with the document to watch verification fail.
# Create the document to sign echo "I, Alice, agree to the terms dated 2024-01-15." > contract.txt # Sign it with your private key openssl dgst -sha256 -sign my_private.pem \ -out contract.sig contract.txt # Verify the signature using your public key openssl dgst -sha256 -verify my_public.pem \ -signature contract.sig contract.txt # Expected output: Verified OK # Now tamper: append a dot to the document echo "." >> contract.txt # Verify again — it should now FAIL openssl dgst -sha256 -verify my_public.pem \ -signature contract.sig contract.txt # Expected output: Verification Failure
Save the script as rsa_demo.py in ~/crypto_labs and run it with python3 rsa_demo.py.
# rsa_demo.py — save this file then run: python3 rsa_demo.py from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) public_key = private_key.public_key() message = b"Meet me at the clock tower." ciphertext = public_key.encrypt( message, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) print(f"Encrypted (first 20 bytes): {ciphertext[:20].hex()}") plaintext = private_key.decrypt( ciphertext, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) print(f"Decrypted: {plaintext.decode()}")
# Run it
python3 rsa_demo.py
1. Alice wants to send Bob an encrypted message. Which key does she use to encrypt it?
2. What does a successful Verified OK tell you about a digitally signed document?
This lab requires your local Kali Linux machine — it cannot be run on Colab or a remote server because it requires access to a real network interface and a locally running browser. Wireshark is pre-installed on Kali. The key step is setting the SSLKEYLOGFILE environment variable in the same terminal session from which you launch Firefox, so both processes share the variable.
# Confirm Wireshark is installed wireshark --version | head -1 # Confirm you are in the wireshark group (set during environment setup) groups | grep wireshark # If not listed: sudo usermod -aG wireshark $USER && newgrp wireshark # Confirm Firefox is installed firefox --version
Core concept — the TLS handshake
TLS sets up an encrypted channel in two phases. The handshake uses asymmetric crypto to authenticate the server and agree on a shared secret. The record layer then uses fast symmetric crypto (AES-GCM or ChaCha20-Poly1305) for all actual data.
Both commands must run in the same terminal session. Setting the environment variable and then launching Firefox from that same session ensures Firefox inherits the variable and writes session keys to the log file.
# Set the key log file path (stays active for this terminal session) export SSLKEYLOGFILE=~/crypto_labs/tls_keys.log # Confirm the variable is set echo $SSLKEYLOGFILE # Expected: /home/kali/crypto_labs/tls_keys.log # Launch Firefox from this same terminal (& runs it in background) firefox & # Visit a simple JSON API in Firefox — easy to spot after decryption # Browse to: https://httpbin.org/get # After visiting the site, confirm the key log was written ls -lh ~/crypto_labs/tls_keys.log cat ~/crypto_labs/tls_keys.log | head -5
rm ~/crypto_labs/tls_keys.log and never use this technique on a machine handling real sensitive data.
You can launch Wireshark from the terminal or from the Kali application menu. Starting from the terminal lets you see any errors.
# Launch Wireshark (GUI will open) wireshark & # Alternatively, capture directly to file from terminal (no GUI needed) sudo tcpdump -i any -w ~/crypto_labs/capture.pcap port 443 & # Browse to https://httpbin.org/get in Firefox, then stop tcpdump: sudo pkill tcpdump # Open the saved capture in Wireshark wireshark ~/crypto_labs/capture.pcap
In Wireshark, type tcp port 443 in the display filter bar. You will see TLS records with Application Data payloads — unreadable binary at this point.
sudo usermod -aG wireshark $USER && newgrp wireshark and restart your terminal.
In Wireshark's filter bar, type tls.handshake to isolate handshake packets. Click a ClientHello packet, expand the TLS layer in the detail pane, and locate:
TLS_AES_256_GCM_SHA384)Then click the Certificate packet and find the server's domain in the Subject Alternative Name (SAN) field.
In Wireshark, go to Edit → Preferences → Protocols → TLS. Set the (Pre)-Master-Secret log filename field to the full path of your keylog file, then click OK.
# Get the full path to paste into Wireshark realpath ~/crypto_labs/tls_keys.log # e.g. /home/kali/crypto_labs/tls_keys.log
Fill in the table below in your own notes using what you find in Wireshark:
| Field | Where to find it in Wireshark | Your observation |
|---|---|---|
| Cipher suite chosen | ServerHello → Cipher Suite | write here |
| Key exchange algorithm | Look for ECDHE or DHE in the suite name | write here |
| Certificate valid until | Certificate → Validity → Not After | write here |
| Certificate issuer (CA) | Certificate → Issuer → CN | write here |
| Encrypted record size | Application Data packet → Length field | write here |
# Remove the session key log and capture file rm ~/crypto_labs/tls_keys.log rm ~/crypto_labs/capture.pcap # Unset the environment variable for this session unset SSLKEYLOGFILE
1. Why can Wireshark decrypt TLS traffic when given the SSLKEYLOGFILE, but a normal network attacker cannot?
2. What is the purpose of the client and server random nonces sent during the handshake?
Hashcat and rockyou.txt are pre-installed on Kali Linux. The rockyou wordlist ships compressed — unzip it once if you have not done so already. Python libraries need a one-time install. All commands run in your terminal from ~/crypto_labs.
# Decompress rockyou if not already done sudo gunzip /usr/share/wordlists/rockyou.txt.gz 2>/dev/null ls -lh /usr/share/wordlists/rockyou.txt # Should show ~134 MB uncompressed # Install Python password hashing libraries pip3 install bcrypt argon2-cffi # Confirm Hashcat is available hashcat --version # Make sure you are in the working directory cd ~/crypto_labs
--force to suppress the CPU-mode warning if needed.
Core concept — why MD5 is not enough for passwords
Cryptographic hash functions are designed to be fast. That is exactly the wrong property for password storage. An attacker with a GPU can compute billions of MD5 hashes per second, so a leaked MD5 database can be cracked with a wordlist in minutes.
Algorithm comparison at a glance
| Algorithm | Speed | Salt? | Stretching? | Suitable for passwords? |
|---|---|---|---|---|
| MD5 | ~10 billion/s (GPU) | Manual only | No | No |
| SHA-256 | ~4 billion/s (GPU) | Manual only | No | No |
| bcrypt | ~20 k/s (cost 12) | Built-in (22 chars) | Yes (cost factor) | Yes (aging) |
| Argon2id | Tunable / slow | Built-in | Yes + memory-hard | Best current |
Save the script as hash_compare.py and run it. Observe the timing difference — it tells you directly how hard each algorithm is to crack.
# Save as ~/crypto_labs/hash_compare.py import hashlib, bcrypt, time from argon2 import PasswordHasher password = b"hunter2" t0 = time.perf_counter() md5_hash = hashlib.md5(password).hexdigest() print(f"MD5 : {md5_hash}") print(f" time: {(time.perf_counter()-t0)*1000:.3f} ms\n") t0 = time.perf_counter() bc_hash = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12)) print(f"bcrypt: {bc_hash.decode()}") print(f" time: {(time.perf_counter()-t0)*1000:.0f} ms\n") ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1) t0 = time.perf_counter() arg_hash = ph.hash(password.decode()) print(f"Argon2: {arg_hash}") print(f" time: {(time.perf_counter()-t0)*1000:.0f} ms")
# Run it
python3 hash_compare.py
Create a file of MD5 hashes, then use Hashcat with rockyou.txt to crack them.
# Generate MD5 hashes of common passwords python3 -c " import hashlib passwords = ['password', 'iloveyou', 'monkey', 'qwerty123', 'hunter2'] for p in passwords: print(hashlib.md5(p.encode()).hexdigest()) " > md5_hashes.txt cat md5_hashes.txt # Run Hashcat: mode 0 = raw MD5, attack mode 0 = dictionary hashcat -m 0 -a 0 md5_hashes.txt /usr/share/wordlists/rockyou.txt # If running in a VM without GPU, add --force to suppress the warning # hashcat -m 0 -a 0 --force md5_hashes.txt /usr/share/wordlists/rockyou.txt # Show cracked results from the potfile hashcat -m 0 md5_hashes.txt --show
Generate a bcrypt hash and watch the H/s (hashes per second) metric in Hashcat. The difference in cracking speed compared to MD5 is the core takeaway.
# Generate a bcrypt hash of "monkey" python3 -c " import bcrypt h = bcrypt.hashpw(b'monkey', bcrypt.gensalt(rounds=12)) print(h.decode()) " > bcrypt_hash.txt cat bcrypt_hash.txt # Attack it — mode 3200 = bcrypt hashcat -m 3200 -a 0 bcrypt_hash.txt /usr/share/wordlists/rockyou.txt # Watch the H/s figure: ~10 billion/s for MD5 vs ~20,000/s for bcrypt # "monkey" is in rockyou so it should still crack — but compare the ETA # for a full rockyou scan vs the MD5 run.
Save and run this script to see how salting ensures two users with the same password store different hashes.
# Save as ~/crypto_labs/salt_demo.py import hashlib, os password = "password123" # Without salt — same input always gives same output h1 = hashlib.sha256(password.encode()).hexdigest() h2 = hashlib.sha256(password.encode()).hexdigest() print(f"Same hash (no salt): {h1 == h2}") # With a unique salt per user salt1 = os.urandom(16).hex() salt2 = os.urandom(16).hex() h1 = hashlib.sha256((salt1 + password).encode()).hexdigest() h2 = hashlib.sha256((salt2 + password).encode()).hexdigest() print(f"Same hash (with salt): {h1 == h2}") print(f"User 1 stored: {salt1}:{h1}") print(f"User 2 stored: {salt2}:{h2}")
# Run it
python3 salt_demo.py
1. An application stores passwords as unsalted SHA-256 hashes. A database breach exposes these hashes. Why is this serious even though SHA-256 is "cryptographically secure"?
2. What is the purpose of the cost factor in bcrypt, and how should you choose its value?
This lab is entirely Python — no special system tools beyond what is pre-installed on Kali. Every script is saved as a .py file in ~/crypto_labs and run with python3. The cryptography library may already be present on Kali; the install command below is safe to run either way.
# Install (or confirm) the cryptography library pip3 install cryptography python3 -c "import cryptography; print(cryptography.__version__)" # Make sure you are in the working directory cd ~/crypto_labs
Core concept — authenticated encryption (AEAD)
Encryption provides confidentiality — attackers cannot read the plaintext. But without authentication, an attacker who can modify ciphertext in transit can cause controlled changes to the decrypted output without knowing the key. AES-GCM solves this with a Galois/Counter Mode authentication tag appended to the ciphertext. Decryption fails immediately if even a single bit has been altered.
Save this as encrypt_file.py in ~/crypto_labs. The key is derived from your passphrase using PBKDF2 — never hard-code keys in source files.
# Save as ~/crypto_labs/encrypt_file.py import os, sys from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes import getpass def derive_key(passphrase, salt): kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=390000) return kdf.derive(passphrase.encode()) def encrypt_file(input_path, output_path, passphrase): salt = os.urandom(16) nonce = os.urandom(12) key = derive_key(passphrase, salt) aesgcm = AESGCM(key) with open(input_path, "rb") as f: plaintext = f.read() ciphertext = aesgcm.encrypt(nonce, plaintext, None) with open(output_path, "wb") as f: f.write(salt + nonce + ciphertext) print(f"Encrypted: {input_path} -> {output_path}") print(f" Salt : {salt.hex()}") print(f" Nonce: {nonce.hex()}") if __name__ == "__main__": pw = getpass.getpass("Passphrase: ") encrypt_file(sys.argv[1], sys.argv[1] + ".enc", pw)
# Create a test file and encrypt it echo "TOP SECRET: Project Nightingale details go here." > secret.txt python3 encrypt_file.py secret.txt # You will be prompted for a passphrase — remember it for decryption # Confirm the output is unreadable binary xxd secret.txt.enc | head -4
Save this as decrypt_file.py. It reads the salt and nonce from the file header — neither is secret; only the passphrase must be protected.
# Save as ~/crypto_labs/decrypt_file.py import sys, getpass from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.exceptions import InvalidTag from encrypt_file import derive_key def decrypt_file(enc_path, out_path, passphrase): with open(enc_path, "rb") as f: data = f.read() salt = data[:16] nonce = data[16:28] ciphertext = data[28:] key = derive_key(passphrase, salt) aesgcm = AESGCM(key) try: plaintext = aesgcm.decrypt(nonce, ciphertext, None) except InvalidTag: print("Decryption failed: auth tag mismatch.") print("The ciphertext was tampered with, or the passphrase is wrong.") sys.exit(1) with open(out_path, "wb") as f: f.write(plaintext) print(f"Decrypted: {enc_path} -> {out_path}") if __name__ == "__main__": pw = getpass.getpass("Passphrase: ") decrypt_file(sys.argv[1], sys.argv[1].replace(".enc", ".dec"), pw)
# Decrypt the file (enter the same passphrase you used to encrypt) python3 decrypt_file.py secret.txt.enc # Compare original and decrypted — should be identical diff secret.txt secret.txt.dec && echo "Files match!"
Flip a single byte in the encrypted file and attempt to decrypt it. AES-GCM's authentication tag will reject it immediately.
# Flip one byte inside the ciphertext (past the 28-byte header) python3 -c " with open('secret.txt.enc', 'rb') as f: data = bytearray(f.read()) data[30] ^= 0xFF # flip byte at position 30 with open('secret_tampered.enc', 'wb') as f: f.write(data) print('Tampered file written.') " # Attempt decryption — should fail with auth tag mismatch python3 decrypt_file.py secret_tampered.enc # Expected: Decryption failed: auth tag mismatch.
AAD lets you authenticate metadata alongside the ciphertext without encrypting it. Save and run this standalone demo.
# Save as ~/crypto_labs/aad_demo.py import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.exceptions import InvalidTag key = AESGCM.generate_key(bit_length=256) nonce = os.urandom(12) aesgcm = AESGCM(key) plaintext = b"Confidential payload" aad = b"recipient:alice,version:1" ciphertext = aesgcm.encrypt(nonce, plaintext, aad) print("Encrypted OK") # Correct AAD — decrypts fine msg = aesgcm.decrypt(nonce, ciphertext, aad) print(f"Decrypted: {msg}") # Wrong AAD — auth failure try: aesgcm.decrypt(nonce, ciphertext, b"recipient:bob,version:1") except InvalidTag: print("Auth tag mismatch: AAD was changed!")
# Run it
python3 aad_demo.py
Fernet is a high-level recipe combining AES-128-CBC + HMAC-SHA256. Simpler to use correctly but less flexible than raw AES-GCM.
# Run inline — no file needed
python3 -c "
from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
token = f.encrypt(b'Secret message')
print(f'Token: {token[:40]}...')
msg = f.decrypt(token)
print(f'Decrypted: {msg}')
"
1. You encrypt two files using AES-256-GCM with the same key. Why is it critical to use a different nonce for each, and what can an attacker do if you reuse a nonce?
2. Why is the salt stored unencrypted at the start of the output file, even though it was used to derive the encryption key?
GPG is pre-installed on Kali Linux. This lab uses only the terminal — no email client is required. You will simulate two users (Alice and Bob) on the same machine using separate GPG home directories, which is a clean way to practice key exchange and email encryption without needing two physical machines or real email accounts.
# Confirm GPG is installed gpg --version | head -2 # Expected: gpg (GnuPG) 2.x.x # Create isolated GPG home directories for Alice and Bob mkdir -p ~/crypto_labs/alice_gpg ~/crypto_labs/bob_gpg chmod 700 ~/crypto_labs/alice_gpg ~/crypto_labs/bob_gpg # Set a shell alias so we can easily switch between them alias gpg-alice='gpg --homedir ~/crypto_labs/alice_gpg' alias gpg-bob='gpg --homedir ~/crypto_labs/bob_gpg' # Make sure you are in the working directory cd ~/crypto_labs
alias commands above only last for your current terminal session. If you close the terminal and reopen it, re-run those two alias lines before continuing. Alternatively, add them to ~/.bashrc to make them permanent.
Core concept 1 — digital signatures in email
A digital signature on an email is created by the sender's private key and verified by anyone who has the sender's public key. It answers two questions: Did this really come from Alice? (authenticity) and Has anything been changed since Alice sent it? (integrity). It does not hide the message content — a signed email is still readable by anyone who intercepts it.
Core concept 2 — digital envelopes in email
A digital envelope is hybrid encryption applied to email. Encrypting the whole message body with RSA is impractical (RSA is slow and has size limits). Instead, a random symmetric session key is generated, the message is encrypted with that fast symmetric key, and the session key itself is encrypted with the recipient's public key. The recipient uses their private key to unwrap the session key, then uses that to decrypt the message.
Each participant needs their own key pair. You are simulating two users on one machine using separate --homedir paths so their keyrings never mix.
# Generate Alice's key pair in her isolated keyring # GPG will open an interactive prompt — fill in the fields below gpg --homedir ~/crypto_labs/alice_gpg --full-generate-key # At the prompts, select: # Key type : (1) RSA and RSA # Key size : 4096 # Expiry : 0 (does not expire — fine for a lab) # Real name : Alice Lab # Email : alice@lab.local # Comment : (leave blank) # Passphrase : choose something simple, e.g. alicepass
# Generate Bob's key pair in his isolated keyring gpg --homedir ~/crypto_labs/bob_gpg --full-generate-key # At the prompts: # Real name : Bob Lab # Email : bob@lab.local # Passphrase : e.g. bobpass
# Confirm both keyrings have their keys
gpg --homedir ~/crypto_labs/alice_gpg --list-keys
gpg --homedir ~/crypto_labs/bob_gpg --list-keys
In a real email workflow, you publish your public key to a keyserver or attach it to an email. Here you will export both keys to files and import them into each other's keyring — simulating the key exchange step.
# Export Alice's public key to a file gpg --homedir ~/crypto_labs/alice_gpg \ --armor --export alice@lab.local \ > alice_public.asc # Export Bob's public key to a file gpg --homedir ~/crypto_labs/bob_gpg \ --armor --export bob@lab.local \ > bob_public.asc # Inspect what the exported key looks like cat alice_public.asc # You will see a -----BEGIN PGP PUBLIC KEY BLOCK----- header
# Import Bob's public key into Alice's keyring # (Alice needs Bob's key to encrypt messages TO him) gpg --homedir ~/crypto_labs/alice_gpg \ --import bob_public.asc # Import Alice's public key into Bob's keyring # (Bob needs Alice's key to verify her signatures) gpg --homedir ~/crypto_labs/bob_gpg \ --import alice_public.asc # Verify both keyrings now contain two keys gpg --homedir ~/crypto_labs/alice_gpg --list-keys gpg --homedir ~/crypto_labs/bob_gpg --list-keys
Alice writes an email and signs it with her private key. The output is a cleartext message with a PGP signature block appended — anyone can read the message, but only Alice's private key could have produced that signature.
# Create the email body file
cat > email_body.txt << 'EOF'
To: bob@lab.local
From: alice@lab.local
Subject: Project update
Hi Bob,
The deployment is scheduled for Friday at 14:00 UTC.
Please confirm receipt.
-- Alice
EOF
# Sign it with Alice's private key (clearsign = readable + signature) gpg --homedir ~/crypto_labs/alice_gpg \ --clearsign \ --local-user alice@lab.local \ email_body.txt # Enter Alice's passphrase when prompted # Output written to: email_body.txt.asc # Inspect the signed message cat email_body.txt.asc # You will see the original text plus a -----BEGIN PGP SIGNATURE----- block
# Bob verifies the signature using Alice's public key (already in his keyring) gpg --homedir ~/crypto_labs/bob_gpg \ --verify email_body.txt.asc # Expected output: # gpg: Good signature from "Alice Lab <alice@lab.local>" # gpg: WARNING: This key is not certified ... (trust warning — ignore for lab)
# Now tamper with the message — edit one word — then verify again sed -i 's/Friday/Saturday/' email_body.txt.asc gpg --homedir ~/crypto_labs/bob_gpg \ --verify email_body.txt.asc # Expected: gpg: BAD signature from "Alice Lab <alice@lab.local>"
Alice encrypts a message to Bob. GPG generates a random session key, encrypts the message body with it, and then wraps the session key with Bob's public key — a digital envelope that only Bob can open.
# Re-create the email body (undo the tamper from Step 3)
cat > secret_email.txt << 'EOF'
To: bob@lab.local
From: alice@lab.local
Subject: Confidential
Bob, the server credentials are: admin / Tr0ub4dor&3
Do not share. Delete after reading.
-- Alice
EOF
# Encrypt to Bob (uses Bob's public key from Alice's keyring) # --armor = ASCII output instead of binary (email-safe) # --recipient = who can decrypt this gpg --homedir ~/crypto_labs/alice_gpg \ --armor \ --encrypt \ --recipient bob@lab.local \ secret_email.txt # Output: secret_email.txt.asc # Inspect the encrypted output — should be unreadable cat secret_email.txt.asc # You will see -----BEGIN PGP MESSAGE----- followed by base64 data
# Bob decrypts using his private key gpg --homedir ~/crypto_labs/bob_gpg \ --decrypt secret_email.txt.asc # Enter Bob's passphrase when prompted # The plaintext message appears on stdout # Redirect decrypted output to a file gpg --homedir ~/crypto_labs/bob_gpg \ --output secret_email_decrypted.txt \ --decrypt secret_email.txt.asc cat secret_email_decrypted.txt
# Try decrypting with Alice's key — it should FAIL # (only Bob's private key can open this envelope) gpg --homedir ~/crypto_labs/alice_gpg \ --decrypt secret_email.txt.asc # Expected: gpg: decryption failed: No secret key
.asc file contains both: the AES-encrypted body and the RSA-encrypted session key packet. Bob's private key unwraps the session key first, then AES decryption recovers the body. This is exactly the digital envelope structure from the diagram above.
In real secure email both operations happen together: Alice signs with her private key (so Bob knows it came from her) and encrypts with Bob's public key (so only Bob can read it). GPG does both in a single command.
# Alice signs AND encrypts in one step gpg --homedir ~/crypto_labs/alice_gpg \ --armor \ --sign \ --encrypt \ --local-user alice@lab.local \ --recipient bob@lab.local \ secret_email.txt # Output: secret_email.txt.asc (signed + encrypted)
# Bob decrypts — GPG will automatically verify the signature too gpg --homedir ~/crypto_labs/bob_gpg \ --decrypt secret_email.txt.asc # Expected output includes: # gpg: Good signature from "Alice Lab <alice@lab.local>" # followed by the decrypted plaintext
GPG's --list-packets flag lets you see the internal structure of a PGP message — the individual packets for the encrypted session key, the encrypted body, the signature, and so on. This is the email equivalent of inspecting a TLS handshake in Wireshark.
# Inspect the signed-and-encrypted message structure gpg --homedir ~/crypto_labs/bob_gpg \ --list-packets secret_email.txt.asc # Expected packet types (from outer to inner): # :pubkey enc packet: — RSA-encrypted session key (for Bob) # :encrypted data packet: — the AES-encrypted payload # :compressed data packet: — GPG compresses before encrypting # :onepass sig packet: — marker that a signature follows # :literal data packet: — the actual message text # :signature packet: — Alice's RSA signature over the message
# Inspect just the public-key encrypted session key packet # Note the key ID — it corresponds to Bob's encryption subkey gpg --homedir ~/crypto_labs/bob_gpg \ --list-packets secret_email.txt.asc 2>&1 | grep -A5 "pubkey enc"
:pubkey enc packet: is the locked box containing the session key — this is the envelope. The :encrypted data packet: is the message inside. The :signature packet: is Alice's seal proving she wrote it. Everything you have built across Labs 1–4 is present here: RSA from Lab 1, hybrid encryption from Lab 4, and integrity verification from both.
A GPG public key is identified by its fingerprint — a SHA-1 or SHA-256 hash of the key material. Verifying fingerprints out-of-band (phone, in-person) is how you confirm a public key genuinely belongs to who you think it does before trusting it. This is the human-layer solution to the key authentication problem.
# View Alice's key fingerprint gpg --homedir ~/crypto_labs/alice_gpg \ --fingerprint alice@lab.local # View Bob's key fingerprint gpg --homedir ~/crypto_labs/bob_gpg \ --fingerprint bob@lab.local # The fingerprint looks like: # pub rsa4096 2024-01-15 [SC] # A1B2 C3D4 E5F6 7890 ABCD EF12 3456 7890 ABCD EF12 # Read it in groups of 4 characters when verifying over the phone
# Mark Bob's key as fully trusted in Alice's keyring # (required to suppress the trust warning when encrypting) gpg --homedir ~/crypto_labs/alice_gpg \ --edit-key bob@lab.local # At the gpg> prompt, type: # trust # 5 (I trust ultimately) # y # quit
This script implements the full digital envelope manually — no GPG — using the same cryptography library from Lab 4. It makes the hybrid encryption structure explicit in code.
# Save as ~/crypto_labs/digital_envelope.py import os from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives import hashes, serialization # ── Key generation (Bob's key pair) ────────────────────────────── bob_private = rsa.generate_private_key(public_exponent=65537, key_size=4096) bob_public = bob_private.public_key() # ── ALICE SENDS: seal the digital envelope ─────────────────────── message = b"Bob, the secret credentials are: admin / Tr0ub4dor&3" # Step 1: generate a random 256-bit AES session key session_key = os.urandom(32) nonce = os.urandom(12) # Step 2: encrypt the message body with the session key (AES-256-GCM) aesgcm = AESGCM(session_key) encrypted_body = aesgcm.encrypt(nonce, message, None) # Step 3: encrypt the session key with Bob's RSA public key (the "envelope seal") encrypted_session_key = bob_public.encrypt( session_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) print(f"[Alice] Envelope sealed.") print(f" Session key (encrypted, first 16 bytes): {encrypted_session_key[:16].hex()}") print(f" Encrypted body (first 16 bytes) : {encrypted_body[:16].hex()}\n") # ── BOB RECEIVES: open the digital envelope ────────────────────── # Step 4: decrypt the session key with Bob's private key recovered_session_key = bob_private.decrypt( encrypted_session_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) # Step 5: use the recovered session key to decrypt the message body aesgcm2 = AESGCM(recovered_session_key) plaintext = aesgcm2.decrypt(nonce, encrypted_body, None) print(f"[Bob] Envelope opened.") print(f" Decrypted message: {plaintext.decode()}")
# Run it
python3 digital_envelope.py
1. Alice sends Bob a signed email. What two things does Bob learn from a successful "Good signature" verification, and what does he not learn?
2. Why does GPG encrypt the message body with a random AES session key instead of directly encrypting it with Bob's RSA public key?
3. Alice signs her email first and then encrypts it. Why is this order more secure than encrypting first and then signing?
4. What is the difference between GPG's Web of Trust and the Certificate Authority model used in TLS?
Workbook complete
Ready for the Advanced labs? Next topics include implementing Diffie-Hellman key exchange from scratch, exploiting a padding oracle, building a full certificate authority with S/MIME email, and simulating hybrid-encryption ransomware in an isolated VM.