By completing these hands-on labs, you will be able to:
Implement symmetric encryption using XOR and observe encryption/decryption cycles
Demonstrate classical ciphers with Caesar cipher and understand its weaknesses
Create hash functions for data integrity verification
Build Message Authentication Codes (MAC) to verify message authenticity
Simulate key exchange using Diffie-Hellman concepts
Visualize encryption operations through LED indicators and serial output
In 60 Seconds
Practical encryption labs walk through implementing AES, HMAC, ECC, and TLS in realistic IoT scenarios, building the hands-on skills needed to apply cryptography correctly on embedded devices.
For Beginners: Encryption Labs & Practice
This hands-on chapter lets you practice cryptographic techniques for IoT security. Think of it as a locksmith training course – you learn to work with encryption tools, set up secure channels, and test your implementations in a safe environment before applying them to real systems.
In Plain English
These labs let you build cryptographic systems on real hardware (simulated ESP32). You’ll see encryption in action - watch data transform into ciphertext, verify integrity with hashes, and experience what happens when attacks occur.
Important: The algorithms in these labs are simplified for educational purposes. Production IoT systems use libraries like mbedTLS with AES-GCM.
How It Works: Lab-Based Cryptography Learning
Understanding cryptography through hands-on labs involves several key steps:
Component Assembly: Wire LEDs and buttons to ESP32 to create visual feedback for encryption operations
Mode Selection: Use buttons to cycle through different encryption algorithms (XOR, Caesar, Hash, MAC, Key Exchange)
Operation Execution: Trigger encryption/decryption operations and observe results via serial monitor and LED indicators
Attack Simulation: Toggle attack mode to see how wrong keys or tampered data cause failures
Visual Verification: LEDs provide immediate feedback - green for success, red for failure, yellow during processing
Each lab builds on previous concepts, starting with simple XOR encryption and progressing to authenticated encryption with key exchange protocols. The Wokwi simulator lets you see cryptographic concepts in action without requiring physical hardware.
Note how the same XOR operation encrypts and decrypts
Press Button 3 to enable “attack mode” (wrong key)
Observe how decryption fails with the wrong key
Activity 2: Hash Integrity
Switch to HASH mode (Button 1)
Execute to see the hash of the original message
Enable attack mode to simulate tampering
Observe how any change produces a completely different hash
Activity 3: Key Exchange
Switch to KEY EXCHANGE mode
Press Button 3 to perform Diffie-Hellman
Observe that Alice and Bob compute the same shared secret
Note that this works over an “insecure” channel
Try It: Caesar Cipher Shift Explorer
Adjust the shift value and type a message to see how the Caesar cipher transforms each letter. Try all 25 possible shifts to see why this cipher is trivially breakable by brute force.
{const msg = (caesarMessage ||"").toUpperCase();const shift = caesarShiftVal;const showAll = caesarShowAllShifts.length>0;functioncaesarEnc(text, s) {let result ="";for (let i =0; i < text.length; i++) {const c = text.charCodeAt(i);if (c >=65&& c <=90) { result +=String.fromCharCode(((c -65+ s) %26) +65); } else { result += text[i]; } }return result; }functioncaesarDec(text, s) {returncaesarEnc(text,26- (s %26)); }const encrypted =caesarEnc(msg, shift);const decrypted =caesarDec(encrypted, shift);const match = decrypted === msg;let bruteForceHtml ="";if (showAll) { bruteForceHtml =`<div style="margin-top: 0.8em; max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 0.5em; background: #fff;"> <strong style="color: #2C3E50;">Brute Force — All 25 Shifts:</strong><br/>${Array.from({length:25}, (_, i) => {const s = i +1;const attempt =caesarDec(encrypted, s);const isCorrect = s === shift;return`<div style="font-family: monospace; padding: 2px 4px; ${isCorrect ?'background: #d4edda; font-weight: bold; border-radius: 3px;':''}"> Shift ${String(s).padStart(2,'\u00a0')}: ${attempt}${isCorrect ?' <span style="color: #16A085;">← CORRECT</span>':''} </div>`; }).join('')} </div>`; }returnhtml`<div style="font-family: Arial, sans-serif; padding: 1em; background: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;"> <div style="margin-bottom: 0.5em;"><strong style="color: #2C3E50;">Plaintext:</strong> <code>${msg}</code></div> <div style="margin-bottom: 0.5em;"><strong style="color: #E67E22;">Encrypted (shift ${shift}):</strong> <code>${encrypted}</code></div> <div style="margin-bottom: 0.5em;"><strong style="color: #16A085;">Decrypted:</strong> <code>${decrypted}</code></div> <div style="display: flex; gap: 0.5em; flex-wrap: wrap; margin-bottom: 0.5em;">${msg.split('').map((ch, i) => {const cc = ch.charCodeAt(0);const isLetter = cc >=65&& cc <=90;const encCh = encrypted[i];return`<div style="text-align: center; padding: 4px 6px; border-radius: 4px; background: ${isLetter ?'#e8f4f8':'#f0f0f0'}; border: 1px solid ${isLetter ?'#3498DB':'#ccc'}; min-width: 28px;"> <div style="font-weight: bold; color: #2C3E50;">${ch}</div> <div style="font-size: 0.7em; color: #7F8C8D;">${isLetter ?'+'+ shift :''}</div> <div style="font-weight: bold; color: #E67E22;">${encCh}</div> </div>`; }).join('')} </div> <div style="padding: 0.5em; border-radius: 4px; background: ${match ?'#d4edda':'#f8d7da'}; color: ${match ?'#155724':'#721c24'}; font-weight: bold;">${match ?'Round-trip verified -- decryption recovers original plaintext.':'Decryption mismatch.'} </div> <div style="margin-top: 0.5em; font-size: 0.85em; color: #7F8C8D;"> Only 25 possible shifts exist, making Caesar cipher trivially breakable. Enable "Show all 25 brute-force attempts" to see every possibility -- an attacker simply reads down the list until they find readable text. </div>${bruteForceHtml} </div>`;}
22.2.6 Interactive: XOR Encryption Visualizer
Type a message and key to see XOR encryption in action. Notice how the same operation encrypts and decrypts – the fundamental property that makes XOR useful in cryptography.
Figure 22.1: Security layers: encryption, integrity hash, and timestamp for replay prevention
Try It: Replay Attack Timing Window
Explore how timestamp-based replay prevention works. Adjust the time window and see which messages would be accepted or rejected based on their age. This demonstrates Scenario 3 from the Lab 2 code above.
Show code
viewof replayWindow = Inputs.range([1,30], {value:5,step:1,label:"Acceptance window (seconds)",width:"100%"})viewof replayMessageAge = Inputs.range([0,60], {value:10,step:0.5,label:"Message age (seconds)",width:"100%"})viewof replayNumPackets = Inputs.range([3,12], {value:6,step:1,label:"Number of test packets",width:"100%"})
The Vigenere cipher improves on Caesar by using a keyword to create multiple shift values.
Task: Modify the Caesar cipher to use a multi-character key like “IOTKEY”.
Hint: For each position i, use key[i % keyLength] to determine the shift amount.
Challenge 2: Add Nonce for Replay Prevention
Timestamps alone may not prevent replay attacks if clocks are not synchronized.
Task: Add a random nonce (number used once) that both parties track to reject duplicate messages.
Steps:
Generate a random nonce for each message
Include nonce in the packet structure
Receiver maintains a list of recently seen nonces
Reject any message with a previously seen nonce
Challenge 3: Implement Rolling Keys
Using the same key forever is dangerous. Implement key rotation.
Task: Create a system where after every 10 messages, derive a new session key:
newKey = hash(oldKey + counter)
Both sender and receiver independently calculate the same new key.
Try It: MAC Authentication Simulator
See how a Message Authentication Code (MAC) combines a message with a secret key to create a tag that verifies both integrity and authenticity. Change the key or tamper with the message to observe authentication failure.
// Receiver code (Gateway)uint8_t key[16]={0x01,0x02,0x03,...,0x10};// Same keyuint8_t nonce[12]={0xAA,0xBB,0xCC,...,0xFF};// Same noncevoid receive_encrypted_data(){uint8_t ciphertext[32];uint8_t tag[16];// Receive ciphertext and tag receive_from_sensor(ciphertext,32); receive_from_sensor(tag,16); mbedtls_gcm_context ctx;uint8_t plaintext[32]; mbedtls_gcm_init(&ctx); mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key,128);int result = mbedtls_gcm_auth_decrypt(&ctx,32, nonce,12, NULL,0, tag,16, ciphertext, plaintext);if(result !=0){ Serial.println("Decryption FAILED!");// ← Always fails!}}
Problem: Decryption fails with “authentication error” every time.
Debugging Process:
Step 1: Verify Key Material
// Add debug output to senderSerial.print("Key: ");for(int i =0; i <16; i++){ Serial.printf("%02X ", key[i]);}Serial.println();Serial.print("Nonce: ");for(int i =0; i <12; i++){ Serial.printf("%02X ", nonce[i]);}Serial.println();// Output:// Key: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10// Nonce: AA BB CC DD EE FF 00 11 22 33 44 55
// Same debug output on receiver// Output:// Key: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 ✓ Match!// Nonce: AA BB CC DD EE FF 00 11 22 33 44 55 ✓ Match!
Keys match - not the problem.
Step 2: Check What’s Being Sent
// Add packet capture on senderSerial.println("Sending packet:");Serial.print(" Ciphertext: ");for(int i =0; i < len; i++){ Serial.printf("%02X ", ciphertext[i]);}Serial.println();Serial.print(" Tag: ");for(int i =0; i <16; i++){ Serial.printf("%02X ", tag[i]);}Serial.println();// Output (message 1):// Ciphertext: 3A 7B 9C 2D 4E ...// Tag: F3 4A 8B 2C 9D ...// Output (message 2 with SAME plaintext):// Ciphertext: 3A 7B 9C 2D 4E ... ← Identical!// Tag: F3 4A 8B 2C 9D ... ← Identical!
FOUND IT! Ciphertext is identical for the same plaintext. This violates the fundamental property of secure encryption: same plaintext with same key should produce DIFFERENT ciphertext each time.
Root Cause: Nonce reuse. The nonce (number used once) is hardcoded and reused for every message.
Hardcoded keys in source - Any key in code can be extracted from firmware
No nonce/IV management - Reusing nonces breaks encryption
Missing authentication - No way to detect tampering
No error paths - Silently fails or crashes
Single-threaded assumptions - Race conditions in multi-tasking environments
Educational comments - “// This is insecure, don’t use in production”
The Rule: Lab code teaches concepts, production code implements security. If your code has ANY of the red flags above, it’s NOT ready for production. Either refactor completely or use a well-tested library (mbedTLS, libsodium, wolfSSL) with proper configuration.
Checklist Before Deploying Encryption Code:
If you can’t check all boxes, do NOT deploy. The cost of a security breach far exceeds the cost of proper implementation.
22.4.1 Interactive: Hash Integrity Checker
Enter a message and optionally modify it to see how even a tiny change produces a completely different hash value, demonstrating the avalanche effect.
{functiondjb2(str) {let hash =5381;for (let i =0; i < str.length; i++) { hash = ((hash <<5) + hash) + str.charCodeAt(i); hash = hash &0xFFFFFFFF; }return hash >>>0; }const orig = hashOriginal ||"";const mod = hashModified ||"";const h1 =djb2(orig);const h2 =djb2(mod);const match = h1 === h2;const h1Hex ="0x"+ h1.toString(16).toUpperCase().padStart(8,'0');const h2Hex ="0x"+ h2.toString(16).toUpperCase().padStart(8,'0');// Count differing bitslet xorVal = h1 ^ h2;let diffBits =0;while (xorVal >0) { diffBits += xorVal &1; xorVal >>>=1; }returnhtml`<div style="font-family: Arial, sans-serif; padding: 1em; background: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1em; margin-bottom: 0.5em;"> <div><strong>Original hash:</strong> <code>${h1Hex}</code></div> <div><strong>Received hash:</strong> <code>${h2Hex}</code></div> </div> <div style="padding: 0.5em; border-radius: 4px; background: ${match ?'#d4edda':'#f8d7da'}; color: ${match ?'#155724':'#721c24'}; font-weight: bold;">${match ?'Integrity verified -- hashes match.':`Tampered! ${diffBits} of 32 bits differ between hashes.`} </div> <div style="margin-top: 0.5em; font-size: 0.85em; color: #6c757d;"> This uses the DJB2 hash (same as the lab code). Try changing a single character in the received message to see the avalanche effect -- roughly half the hash bits flip. </div> </div>`;}
Putting Numbers to It
Context: Understanding cryptographic strength through practical examples.
XOR Key Reuse Vulnerability: With a 6-byte key (48 bits), brute force requires testing \(2^{48} \approx 2.8 \times 10^{14}\) keys. At 1 million keys/second, this takes \(2.8 \times 10^8\) seconds ≈ 8.9 years. Worked example: But with XOR key reuse, if attacker has two ciphertexts \(C_1\) and \(C_2\) encrypted with same key \(K\): \(C_1 \oplus C_2 = (P_1 \oplus K) \oplus (P_2 \oplus K) = P_1 \oplus P_2\) (keystream cancels). If attacker knows \(P_1\) (e.g., “HTTP/1.1”), they compute \(P_2 = P_1 \oplus C_1 \oplus C_2\) instantly—no key needed.
Caesar Cipher Keyspace: With 26-letter alphabet, only 25 possible shifts (shift of 0 or 26 is identity). Brute force tries all 25 in <1 second. Worked example: For text “SENSOR:TEMP=25C” (shift 3), attacker tries all shifts until they see readable English—trivial attack.
Diffie-Hellman Security: Lab uses prime \(p=23\), base \(g=5\), private keys \(a=6\), \(b=15\). Computing \(g^a \bmod p = 5^6 \bmod 23 = 15625 \bmod 23 = 8\). Production uses 2048-bit primes where discrete log is computationally infeasible. Worked example: With 2048-bit \(p\), attacker must solve \(g^x \equiv y \pmod{p}\) for \(x\)—estimated \(2^{112}\) operations (~\(10^{33}\) years at 1 billion ops/sec).
Hash Collision Probability (Birthday Paradox): For simple 32-bit DJB2 hash, collision probability exceeds 50% with just \(\sqrt{2^{32}} \approx 65{,}536\) messages. SHA-256 (256 bits) requires \(\sqrt{2^{256}} \approx 2^{128}\) messages for 50% collision–computationally infeasible. Worked example: If a system hashes 1 million messages per day, DJB2 reaches 50% collision probability in under 2 hours (only ~65,536 messages needed). SHA-256 takes \(2^{128} / 10^6 \approx 3.4 \times 10^{32}\) years.
Try It: Brute Force Time Calculator
Compare how key length affects the time required to break encryption by brute force. Select an algorithm and attacker speed to see why longer keys provide exponentially more security.
Key Dependencies: XOR provides the foundation for understanding AES operations. Hash functions enable both integrity checks (standalone) and authentication (when combined with keys in MACs). Diffie-Hellman solves the key distribution problem that makes symmetric encryption practical at scale.
Common Pitfalls
1. Using Software RNG Without Hardware Entropy
Software pseudorandom number generators seeded from predictable values (system time, boot count) produce guessable keys. Always seed from a hardware RNG or entropy pool with true randomness sources.
2. Not Pinning the TLS Certificate in Labs
Lab exercises that skip certificate pinning pass against a test CA but would be vulnerable in production. Practice configuring certificate pinning as part of every TLS lab, even in development environments.
3. Forgetting to Zeroize Key Material After Use
Sensitive key bytes remaining in stack or heap memory can be recovered by memory forensics. Call memset (or a compiler-safe alternative like explicit_bzero) to clear key buffers immediately after use.
4. Assuming Lab Success Equals Production Readiness
Lab cryptography exercises use simplified key management and fixed test vectors. Production deployments require proper certificate authorities, key rotation, revocation infrastructure, and hardware security elements.