22  Encryption Labs & Practice

22.1 Learning Objectives

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.

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:

  1. Component Assembly: Wire LEDs and buttons to ESP32 to create visual feedback for encryption operations
  2. Mode Selection: Use buttons to cycle through different encryption algorithms (XOR, Caesar, Hash, MAC, Key Exchange)
  3. Operation Execution: Trigger encryption/decryption operations and observe results via serial monitor and LED indicators
  4. Attack Simulation: Toggle attack mode to see how wrong keys or tampered data cause failures
  5. 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.

22.2 Lab 1: Basic Cryptographic Operations

22.2.1 Components

Component Purpose Wokwi Element
ESP32 DevKit Main controller esp32:devkit-v1
Green LED Success indicator led:green
Red LED Failure indicator led:red
Yellow LED Processing indicator led:yellow
Blue LED Key exchange status led:blue
Push Button 1 Cycle through modes button
Push Button 2 Execute operation button
Push Button 3 Key exchange / attack mode button

22.2.2 Circuit Connections

ESP32 Pin Connections:
----------------------
GPIO 2  --> Green LED (+)  --> 220 ohm Resistor --> GND
GPIO 4  --> Red LED (+)    --> 220 ohm Resistor --> GND
GPIO 5  --> Yellow LED (+) --> 220 ohm Resistor --> GND
GPIO 18 --> Blue LED (+)   --> 220 ohm Resistor --> GND
GPIO 15 --> Button 1       --> GND  (Mode Select)
GPIO 16 --> Button 2       --> GND  (Execute)
GPIO 17 --> Button 3       --> GND  (Key Exchange)

22.2.3 Interactive Wokwi Simulator

Simulator Tips
  • Wire first: Connect all components before pasting code
  • Paste code: Copy the complete code into the editor
  • Run: Click the green “Play” button
  • Serial Monitor: View output in the Serial Monitor panel
  • Buttons: Click on-screen buttons to interact

22.2.4 Lab Code

// =====================================================
// Interactive Cryptographic Operations Lab - ESP32
// Demonstrates: XOR, Caesar, Hash, MAC, Key Exchange
// Educational purposes - NOT for production use
// =====================================================

// Pin Definitions
#define LED_SUCCESS   2
#define LED_FAILURE   4
#define LED_PROCESSING 5
#define LED_KEYEXCHANGE 18

#define BTN_MODE      15
#define BTN_EXECUTE   16
#define BTN_KEYEXCHANGE 17

// Crypto Mode Enumeration
enum CryptoMode {
  MODE_XOR = 0,
  MODE_CAESAR = 1,
  MODE_HASH = 2,
  MODE_MAC = 3,
  MODE_KEYEXCHANGE = 4,
  MODE_COUNT = 5
};

// Global Variables
CryptoMode currentMode = MODE_XOR;
bool attackMode = false;
String sharedKey = "SECRET";
int caesarShift = 3;
unsigned long lastDebounce = 0;
const unsigned long debounceDelay = 200;

// Diffie-Hellman parameters (educational only)
const int DH_PRIME = 23;
const int DH_BASE = 5;
int privateKeyA = 6;
int privateKeyB = 15;

// Sample messages
String messages[] = {
  "SENSOR:TEMP=25C",
  "ALERT:MOTION_DETECTED",
  "CMD:UNLOCK_DOOR",
  "DATA:HUMIDITY=60%",
  "STATUS:ONLINE"
};
int messageIndex = 0;

void setup() {
  Serial.begin(115200);

  pinMode(LED_SUCCESS, OUTPUT);
  pinMode(LED_FAILURE, OUTPUT);
  pinMode(LED_PROCESSING, OUTPUT);
  pinMode(LED_KEYEXCHANGE, OUTPUT);

  pinMode(BTN_MODE, INPUT_PULLUP);
  pinMode(BTN_EXECUTE, INPUT_PULLUP);
  pinMode(BTN_KEYEXCHANGE, INPUT_PULLUP);

  ledStartupSequence();

  Serial.println("\n================================================");
  Serial.println("   Interactive Cryptographic Operations Lab");
  Serial.println("================================================");
  Serial.println("\nControls:");
  Serial.println("  Button 1 (GPIO 15): Cycle crypto mode");
  Serial.println("  Button 2 (GPIO 16): Execute operation");
  Serial.println("  Button 3 (GPIO 17): Key exchange / Toggle attack");
  Serial.println("================================================\n");

  printCurrentMode();
}

void loop() {
  if (digitalRead(BTN_MODE) == LOW && millis() - lastDebounce > debounceDelay) {
    lastDebounce = millis();
    cycleMode();
  }

  if (digitalRead(BTN_EXECUTE) == LOW && millis() - lastDebounce > debounceDelay) {
    lastDebounce = millis();
    executeCurrentOperation();
  }

  if (digitalRead(BTN_KEYEXCHANGE) == LOW && millis() - lastDebounce > debounceDelay) {
    lastDebounce = millis();
    if (currentMode == MODE_KEYEXCHANGE) {
      performKeyExchange();
    } else {
      toggleAttackMode();
    }
  }
}

// LED Control
void ledStartupSequence() {
  int leds[] = {LED_SUCCESS, LED_FAILURE, LED_PROCESSING, LED_KEYEXCHANGE};
  for (int i = 0; i < 4; i++) {
    digitalWrite(leds[i], HIGH);
    delay(150);
    digitalWrite(leds[i], LOW);
  }
}

void showProcessing() {
  digitalWrite(LED_PROCESSING, HIGH);
  digitalWrite(LED_SUCCESS, LOW);
  digitalWrite(LED_FAILURE, LOW);
}

void showSuccess() {
  digitalWrite(LED_PROCESSING, LOW);
  digitalWrite(LED_SUCCESS, HIGH);
  delay(500);
  digitalWrite(LED_SUCCESS, LOW);
}

void showFailure() {
  digitalWrite(LED_PROCESSING, LOW);
  digitalWrite(LED_FAILURE, HIGH);
  delay(500);
  digitalWrite(LED_FAILURE, LOW);
}

// Mode Management
void cycleMode() {
  currentMode = (CryptoMode)((currentMode + 1) % MODE_COUNT);
  messageIndex = (messageIndex + 1) % 5;
  printCurrentMode();
}

void printCurrentMode() {
  Serial.println("\n----------------------------------------");
  Serial.print("Current Mode: ");
  switch (currentMode) {
    case MODE_XOR:
      Serial.println("XOR ENCRYPTION");
      break;
    case MODE_CAESAR:
      Serial.println("CAESAR CIPHER");
      break;
    case MODE_HASH:
      Serial.println("HASH FUNCTION");
      break;
    case MODE_MAC:
      Serial.println("MESSAGE AUTHENTICATION CODE");
      break;
    case MODE_KEYEXCHANGE:
      Serial.println("KEY EXCHANGE");
      break;
  }
  Serial.print("Sample message: ");
  Serial.println(messages[messageIndex]);
  Serial.println("----------------------------------------");
}

void toggleAttackMode() {
  attackMode = !attackMode;
  Serial.print("\nAttack mode: ");
  Serial.println(attackMode ? "ENABLED" : "DISABLED");
}

// Execute Operations
void executeCurrentOperation() {
  showProcessing();

  switch (currentMode) {
    case MODE_XOR:
      demonstrateXOR();
      break;
    case MODE_CAESAR:
      demonstrateCaesar();
      break;
    case MODE_HASH:
      demonstrateHash();
      break;
    case MODE_MAC:
      demonstrateMAC();
      break;
    case MODE_KEYEXCHANGE:
      Serial.println("Press Button 3 to perform key exchange");
      showFailure();
      break;
  }
}

// XOR Encryption
void demonstrateXOR() {
  Serial.println("\n========== XOR ENCRYPTION ==========");
  String plaintext = messages[messageIndex];
  String key = attackMode ? "WRONG" : sharedKey;

  Serial.print("Plaintext:  ");
  Serial.println(plaintext);
  Serial.print("Key:        ");
  Serial.println(key);

  String ciphertext = xorEncrypt(plaintext, sharedKey);
  Serial.print("Ciphertext: ");
  printHex(ciphertext);

  String decrypted = xorEncrypt(ciphertext, key);
  Serial.print("Decrypted:  ");
  Serial.println(decrypted);

  if (decrypted == plaintext) {
    Serial.println("\n[SUCCESS] Decryption verified!");
    showSuccess();
  } else {
    Serial.println("\n[FAILURE] Wrong key!");
    showFailure();
  }
}

String xorEncrypt(String text, String key) {
  String result = "";
  for (int i = 0; i < text.length(); i++) {
    result += (char)(text[i] ^ key[i % key.length()]);
  }
  return result;
}

// Caesar Cipher
void demonstrateCaesar() {
  Serial.println("\n========== CAESAR CIPHER ==========");
  String plaintext = messages[messageIndex];
  int shift = attackMode ? (caesarShift + 5) % 26 : caesarShift;

  Serial.print("Plaintext: ");
  Serial.println(plaintext);
  Serial.print("Shift: ");
  Serial.println(shift);

  String encrypted = caesarEncrypt(plaintext, caesarShift);
  Serial.print("Encrypted: ");
  Serial.println(encrypted);

  String decrypted = caesarDecrypt(encrypted, shift);
  Serial.print("Decrypted: ");
  Serial.println(decrypted);

  if (decrypted == plaintext) {
    Serial.println("\n[SUCCESS] Caesar cipher verified!");
    showSuccess();
  } else {
    Serial.println("\n[FAILURE] Wrong shift!");
    showFailure();
  }
}

String caesarEncrypt(String text, int shift) {
  String result = "";
  for (int i = 0; i < text.length(); i++) {
    char c = text[i];
    if (c >= 'A' && c <= 'Z') {
      c = ((c - 'A' + shift) % 26) + 'A';
    }
    result += c;
  }
  return result;
}

String caesarDecrypt(String text, int shift) {
  return caesarEncrypt(text, 26 - (shift % 26));
}

// Hash Function
void demonstrateHash() {
  Serial.println("\n========== HASH FUNCTION ==========");
  String data = messages[messageIndex];
  String tamperedData = attackMode ? (data + "X") : data;

  Serial.print("Original: ");
  Serial.println(data);

  unsigned long originalHash = simpleHash(data);
  Serial.print("Hash: 0x");
  Serial.println(originalHash, HEX);

  if (attackMode) {
    Serial.print("Tampered: ");
    Serial.println(tamperedData);
  }

  unsigned long receivedHash = simpleHash(tamperedData);

  if (originalHash == receivedHash) {
    Serial.println("\n[VERIFIED] Integrity confirmed!");
    showSuccess();
  } else {
    Serial.println("\n[TAMPERED] Data modified!");
    showFailure();
  }
}

unsigned long simpleHash(String text) {
  unsigned long hash = 5381;
  for (int i = 0; i < text.length(); i++) {
    hash = ((hash << 5) + hash) + text[i];
  }
  return hash;
}

// MAC
void demonstrateMAC() {
  Serial.println("\n========== MAC ==========");
  String data = messages[messageIndex];
  String key = attackMode ? "FAKE_KEY" : sharedKey;

  Serial.print("Data: ");
  Serial.println(data);

  unsigned long originalMAC = simpleHash(data + sharedKey);
  Serial.print("Original MAC: 0x");
  Serial.println(originalMAC, HEX);

  unsigned long receivedMAC = simpleHash(data + key);
  Serial.print("Received MAC: 0x");
  Serial.println(receivedMAC, HEX);

  if (originalMAC == receivedMAC) {
    Serial.println("\n[AUTHENTICATED] MAC verified!");
    showSuccess();
  } else {
    Serial.println("\n[FORGED] Wrong key or tampered!");
    showFailure();
  }
}

// Key Exchange
void performKeyExchange() {
  Serial.println("\n========== DIFFIE-HELLMAN ==========");
  digitalWrite(LED_KEYEXCHANGE, HIGH);

  int publicA = modPow(DH_BASE, privateKeyA, DH_PRIME);
  int publicB = modPow(DH_BASE, privateKeyB, DH_PRIME);

  Serial.print("Alice public: ");
  Serial.println(publicA);
  Serial.print("Bob public: ");
  Serial.println(publicB);

  int secretA = modPow(publicB, privateKeyA, DH_PRIME);
  int secretB = modPow(publicA, privateKeyB, DH_PRIME);

  Serial.print("Alice secret: ");
  Serial.println(secretA);
  Serial.print("Bob secret: ");
  Serial.println(secretB);

  if (secretA == secretB) {
    Serial.println("\n[SUCCESS] Shared secret established!");
    showSuccess();
  } else {
    showFailure();
  }

  digitalWrite(LED_KEYEXCHANGE, LOW);
}

int modPow(int base, int exp, int mod) {
  int result = 1;
  base = base % mod;
  while (exp > 0) {
    if (exp % 2 == 1) {
      result = (result * base) % mod;
    }
    exp = exp >> 1;
    base = (base * base) % mod;
  }
  return result;
}

void printHex(String text) {
  for (int i = 0; i < text.length(); i++) {
    if ((byte)text[i] < 16) Serial.print("0");
    Serial.print((byte)text[i], HEX);
    Serial.print(" ");
  }
  Serial.println();
}

22.2.5 Lab Activities

Activity 1: XOR Encryption

  1. Run the simulation and observe the XOR mode
  2. Press Button 2 to encrypt a message
  3. Note how the same XOR operation encrypts and decrypts
  4. Press Button 3 to enable “attack mode” (wrong key)
  5. Observe how decryption fails with the wrong key

Activity 2: Hash Integrity

  1. Switch to HASH mode (Button 1)
  2. Execute to see the hash of the original message
  3. Enable attack mode to simulate tampering
  4. Observe how any change produces a completely different hash

Activity 3: Key Exchange

  1. Switch to KEY EXCHANGE mode
  2. Press Button 3 to perform Diffie-Hellman
  3. Observe that Alice and Bob compute the same shared secret
  4. 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.

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.

22.2.7 Interactive: Diffie-Hellman Key Exchange Explorer

Experiment with different private keys to see how Alice and Bob arrive at the same shared secret, even though they never exchange private values.


22.3 Lab 2: Secure Message Exchange

This lab demonstrates a complete secure communication protocol with encryption, integrity, and timestamps.

22.3.1 Learning Goals

  • Combine multiple cryptographic primitives
  • Implement replay attack prevention
  • Understand authenticated encryption concepts
// Secure Message Exchange - ESP32
// Demonstrates: Encryption + Integrity + Replay Prevention

struct SecurePacket {
  String senderId;
  unsigned long timestamp;
  String encryptedPayload;
  unsigned long integrityHash;
};

const String SHARED_KEY = "IOT_SECRET_KEY";

void setup() {
  Serial.begin(115200);
  Serial.println("Secure Communication Demo");

  // Scenario 1: Normal communication
  Serial.println("\n=== SCENARIO 1: Normal Communication ===");

  SecurePacket packet = createSecurePacket("SENSOR_A", "temp=25.5C", SHARED_KEY);

  Serial.println("Sender creates secure packet:");
  Serial.print("  Sender ID: ");
  Serial.println(packet.senderId);
  Serial.print("  Timestamp: ");
  Serial.println(packet.timestamp);
  Serial.print("  Encrypted: ");
  printHex(packet.encryptedPayload);
  Serial.print("  Hash: 0x");
  Serial.println(packet.integrityHash, HEX);

  // Receiver verifies
  bool verified = verifySecurePacket(packet, SHARED_KEY);
  if (verified) {
    String decrypted = xorEncrypt(packet.encryptedPayload, SHARED_KEY);
    Serial.print("\n[VERIFIED] Decrypted: ");
    Serial.println(decrypted);
  }

  // Scenario 2: Tampering detection
  Serial.println("\n=== SCENARIO 2: Tampering Detection ===");

  SecurePacket tamperedPacket = packet;
  tamperedPacket.encryptedPayload[0] ^= 0xFF; // Flip bits

  verified = verifySecurePacket(tamperedPacket, SHARED_KEY);
  if (!verified) {
    Serial.println("[ALERT] Integrity check FAILED!");
    Serial.println("[ACTION] Packet rejected");
  }

  // Scenario 3: Replay attack prevention
  Serial.println("\n=== SCENARIO 3: Replay Prevention ===");

  unsigned long currentTime = packet.timestamp + 10000; // 10 seconds later
  unsigned long maxAge = 5000; // 5 second window

  if (currentTime - packet.timestamp > maxAge) {
    Serial.println("[ALERT] Packet too old - REPLAY ATTACK!");
    Serial.println("[ACTION] Packet rejected");
  }
}

void loop() {}

SecurePacket createSecurePacket(String sender, String payload, String key) {
  SecurePacket pkt;
  pkt.senderId = sender;
  pkt.timestamp = millis();
  pkt.encryptedPayload = xorEncrypt(payload, key);

  String toHash = sender + String(pkt.timestamp) + pkt.encryptedPayload + key;
  pkt.integrityHash = simpleHash(toHash);

  return pkt;
}

bool verifySecurePacket(SecurePacket pkt, String key) {
  String toHash = pkt.senderId + String(pkt.timestamp) + pkt.encryptedPayload + key;
  return (simpleHash(toHash) == pkt.integrityHash);
}

String xorEncrypt(String text, String key) {
  String result = "";
  for (int i = 0; i < text.length(); i++) {
    result += (char)(text[i] ^ key[i % key.length()]);
  }
  return result;
}

unsigned long simpleHash(String text) {
  unsigned long hash = 5381;
  for (int i = 0; i < text.length(); i++) {
    hash = ((hash << 5) + hash) + text[i];
  }
  return hash;
}

void printHex(String text) {
  for (int i = 0; i < text.length(); i++) {
    if ((byte)text[i] < 16) Serial.print("0");
    Serial.print((byte)text[i], HEX);
    Serial.print(" ");
  }
  Serial.println();
}

22.3.2 Security Layers Demonstrated

Security architecture diagram showing three layered defenses for IoT communication: encryption for confidentiality, integrity hash for tamper detection, and timestamp validation for replay attack prevention
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.


22.4 Challenge Exercises

Challenge 1: Implement Vigenere Cipher

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:

  1. Generate a random nonce for each message
  2. Include nonce in the packet structure
  3. Receiver maintains a list of recently seen nonces
  4. 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.


Educational vs Production Security

This lab uses simplified algorithms for learning. Real IoT security requires:

Lab Demo Production System
XOR encryption AES-128-GCM or AES-256-GCM
Caesar cipher Not used (educational only)
DJB2 hash SHA-256 or SHA-3
Simple hash auth HMAC-SHA256
Hardcoded key TLS key exchange + certificates

Never deploy lab code in production. Use libraries like:

  • ESP32: mbedTLS (built into ESP-IDF)
  • Arduino: Crypto library or AES library
  • DTLS: tinydtls for constrained devices

Scenario: A student implements AES-128-GCM encryption for an IoT sensor but messages consistently fail verification at the receiver.

Initial Implementation (WRONG):

// Sender code (ESP32)
#include <mbedtls/gcm.h>

uint8_t key[16] = {0x01, 0x02, 0x03, ..., 0x10};  // Pre-shared key
uint8_t nonce[12] = {0xAA, 0xBB, 0xCC, ..., 0xFF};  // Fixed nonce

void send_encrypted_data(const uint8_t *data, size_t len) {
    mbedtls_gcm_context ctx;
    mbedtls_gcm_init(&ctx);
    mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128);

    uint8_t ciphertext[len];
    uint8_t tag[16];

    mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT,
                               len, nonce, 12, NULL, 0,
                               data, ciphertext, 16, tag);

    // Send: ciphertext + tag (not sending nonce!)
    send_to_gateway(ciphertext, len);
    send_to_gateway(tag, 16);

    mbedtls_gcm_free(&ctx);
}
// Receiver code (Gateway)
uint8_t key[16] = {0x01, 0x02, 0x03, ..., 0x10};  // Same key
uint8_t nonce[12] = {0xAA, 0xBB, 0xCC, ..., 0xFF};  // Same nonce

void 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 sender
Serial.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 sender
Serial.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.

Why This Breaks Security:

GCM encryption: Ciphertext = Plaintext ⊕ AES_CTR(Key, Nonce, Counter)

With fixed nonce: - Message 1: C1 = P1 ⊕ AES_CTR(Key, Nonce, 0) - Message 2: C2 = P2 ⊕ AES_CTR(Key, Nonce, 0)

An attacker can XOR the two ciphertexts:

C1 ⊕ C2 = (P1 ⊕ AES_CTR(...)) ⊕ (P2 ⊕ AES_CTR(...))
        = P1 ⊕ P2  ← Keystream cancels out!

If attacker knows P1, they can compute P2. Worse, AES-GCM authentication completely breaks with nonce reuse.

The Fix:

// CORRECT: Generate random nonce for EACH message
#include <esp_random.h>

void send_encrypted_data_correct(const uint8_t *data, size_t len) {
    mbedtls_gcm_context ctx;
    mbedtls_gcm_init(&ctx);
    mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128);

    uint8_t nonce[12];
    esp_fill_random(nonce, 12);  // ← NEW: Random nonce each time

    uint8_t ciphertext[len];
    uint8_t tag[16];

    mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT,
                               len, nonce, 12, NULL, 0,
                               data, ciphertext, 16, tag);

    // Send: nonce + ciphertext + tag (send ALL three!)
    send_to_gateway(nonce, 12);    // ← NEW: Must send nonce
    send_to_gateway(ciphertext, len);
    send_to_gateway(tag, 16);

    mbedtls_gcm_free(&ctx);
}
// Receiver: Receive nonce from sender
void receive_encrypted_data_correct() {
    uint8_t nonce[12];
    uint8_t ciphertext[32];
    uint8_t tag[16];

    // Receive all three components
    receive_from_sensor(nonce, 12);     // ← NEW: Receive nonce
    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 SUCCESS!");
        // Process plaintext...
    } else {
        Serial.println("Authentication failed");
    }
}

Verification:

Message 1:
  Nonce: 3A 7B 9C 2D ...
  Ciphertext: F4 A2 8D ...
  Tag: 9C 3F 7A ...

Message 2 (same plaintext):
  Nonce: B7 4E 1C 9F ...  ← Different nonce
  Ciphertext: 2A 9B 4F ...  ← Different ciphertext!
  Tag: 4D 8A 2C ...  ← Different tag!

✓ Decryption succeeds every time
✓ Different ciphertext for same plaintext
✓ Security properties maintained

Lessons Learned:

  1. Never reuse nonces in AES-GCM (or any CTR-mode cipher)
  2. Send the nonce - receiver needs it to decrypt
  3. Nonces don’t need to be secret - they can be sent in cleartext
  4. Debug incrementally - verify key material, then transmitted data, then protocol
  5. Use a library - mbedTLS handles the crypto, but YOU handle protocol correctness

When implementing encryption in labs or real projects, select the appropriate mode:

Requirement Recommended Mode Why
Confidentiality only AES-CTR Fast, parallelizable, but NO integrity
Confidentiality + integrity AES-GCM Authenticated encryption (AEAD)
No hardware AES ChaCha20-Poly1305 Software-friendly AEAD, no lookup tables
Hardware AES available AES-GCM Leverage hardware acceleration
Educational demo AES-ECB Simple to understand (but NEVER in production!)
Streaming data AES-GCM or ChaCha20 Process data in chunks
Legacy compatibility AES-CBC + HMAC Separate encryption and MAC

Decision Tree for Lab Selection:

What is your primary learning goal?

├─ Understand basic encryption concepts
│  └─ Use XOR (Level 1)
│     Pro: Simple, shows core XOR property
│     Con: Completely insecure
│
├─ Learn modern symmetric encryption
│  └─ Does your MCU have hardware AES?
│     ├─ YES → Use AES-128-GCM
│     │   Pro: Fast, authenticated, standard
│     └─ NO → Use ChaCha20-Poly1305
│         Pro: Fast in software, authenticated
│
├─ Learn asymmetric key exchange
│  └─ Use ECDH with Curve25519
│     Pro: Small keys, side-channel resistant
│
└─ Build complete secure protocol
   └─ Use TLS 1.3 library
      Pro: Industry standard, battle-tested

Lab Complexity Levels:

Level 1: XOR Cipher (1 hour) - Learn: XOR property, key reuse vulnerability - Implement: 10 lines of code - Security: None (educational only)

void xor_encrypt(uint8_t *data, const uint8_t *key, size_t len) {
    for (size_t i = 0; i < len; i++) {
        data[i] ^= key[i % KEY_LEN];
    }
}

Level 2: AES-GCM (2-3 hours) - Learn: Authenticated encryption, nonce management - Implement: 50 lines with library - Security: Production-grade if done correctly

Level 3: TLS (4-6 hours) - Learn: Certificate validation, handshake, session management - Implement: 100+ lines - Security: Production-grade

Common Lab Pitfalls and Fixes:

Mistake Symptom Fix
Nonce reuse Same ciphertext for same plaintext Generate random nonce per message
Missing MAC Data corruption not detected Use AES-GCM (built-in MAC)
Fixed key in code Key visible in firmware Provision unique keys at manufacturing
No key exchange Pre-shared key problem Implement ECDH key exchange
Buffer overflow Crashes, corrupted data Check buffer sizes before encrypt/decrypt
Common Mistake: Using Lab Code in Production

The Mistake: Students copy educational encryption code from labs into production IoT products without understanding its limitations.

Lab Code Characteristics:

  • Simplified for learning
  • Missing error handling
  • Fixed/hardcoded keys
  • No key rotation
  • Minimal resource usage
  • Single-threaded

Production Code Requirements:

  • Robust error handling
  • Secure key management
  • Key rotation support
  • Thread-safe operations
  • Side-channel resistance
  • Proper memory clearing

Example - Lab vs Production:

Lab Code (Educational):

// Simple XOR encryption (LAB ONLY)
void lab_encrypt(uint8_t *data, size_t len) {
    const uint8_t key[] = {0xDE, 0xAD, 0xBE, 0xEF};
    for (size_t i = 0; i < len; i++) {
        data[i] ^= key[i % 4];
    }
}

void loop() {
    uint8_t sensor_data[4] = {25, 0, 0, 0};
    lab_encrypt(sensor_data, 4);
    send_to_cloud(sensor_data, 4);
}

Issues if used in production:

  • ❌ XOR with short key is trivially breakable
  • ❌ Fixed key hardcoded in firmware
  • ❌ No authentication - attacker can modify data
  • ❌ No key rotation - compromise is permanent
  • ❌ No error handling

Production Code (Industry-Grade):

#include <mbedtls/gcm.h>
#include <esp_random.h>
#include "secure_storage.h"  // Hardware-protected key storage

class SecureChannel {
    mbedtls_gcm_context ctx;
    uint8_t device_key[32];     // AES-256 from secure storage
    uint32_t message_counter;   // Replay prevention

public:
    SecureChannel() : message_counter(0) {
        load_device_key(device_key, 32);  // Not hardcoded!
        mbedtls_gcm_init(&ctx);
        mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, device_key, 256);
    }
    ~SecureChannel() {
        mbedtls_platform_zeroize(device_key, 32);  // Secure erase
        mbedtls_gcm_free(&ctx);
    }

    bool encrypt_and_send(const uint8_t *plaintext, size_t len) {
        uint8_t nonce[12];
        esp_fill_random(nonce, 12);  // Random nonce per message
        uint32_t counter = message_counter++;

        uint8_t ciphertext[len], tag[16];
        mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, len,
            nonce, 12, (uint8_t*)&counter, 4,  // counter as AAD
            plaintext, ciphertext, 16, tag);

        // Wire format: nonce || counter || ciphertext || tag
        return send_to_cloud(nonce, counter, ciphertext, len, tag);
    }
};

Key Differences:

Aspect Lab Code Production Code
Encryption XOR AES-256-GCM
Key storage Hardcoded Secure storage
Authentication None Built-in (GCM tag)
Replay prevention None Message counter
Error handling None Comprehensive
Memory management Stack only Dynamic with cleanup
Key lifecycle Static Rotation support
Side channels Vulnerable Constant-time ops

Red Flags in Lab Code:

  1. Hardcoded keys in source - Any key in code can be extracted from firmware
  2. No nonce/IV management - Reusing nonces breaks encryption
  3. Missing authentication - No way to detect tampering
  4. No error paths - Silently fails or crashes
  5. Single-threaded assumptions - Race conditions in multi-tasking environments
  6. 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.

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.

Concept Relationships
Concept Builds On Enables Related To
XOR Encryption Binary operations Stream ciphers, one-time pads AES foundation, symmetric encryption
Caesar Cipher Substitution ciphers Understanding cipher weaknesses Historical cryptography, ROT13
Hash Functions One-way functions Data integrity, digital signatures SHA-256, blockchain
MAC (Message Authentication Code) Hash functions + secret keys Authenticated encryption HMAC, AES-GCM authentication
Diffie-Hellman Modular arithmetic, discrete logarithm Secure key exchange without pre-shared secrets ECDH, TLS handshakes
Lab Code vs Production Educational simplification Understanding crypto pitfalls mbedTLS library, secure coding

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

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.

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.

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.

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.

:

22.5 What’s Next

If you want to… Read this
Review encryption architecture and levels Encryption Architecture & Levels
Explore interactive browser-based tools Interactive Cryptography Tools
Try encryption games for intuition building Encryption Games
Test knowledge with quiz and review Labs, Quiz & Review

Continue to Cipher Challenge Game to test your cryptography knowledge through interactive puzzles and progressively challenging cipher challenges.