Sitemap
Mohamed Amine Barhoumi

Mohamed Amine Barhoumi

Engineering student at ESIEA | Computer vision and embedded AI | Building on-device models for connected beehives (BeeSafe) | ESP32, YOLO, ONNX

Secure MQTT Communication with ESP32: AES Encryption & Decryption (Step-by-Step)

9 min readSep 14, 2025

--

Here is a pdf you can download has all the information about the encryption and all you need step-by-step to understand the encryption and encoding of the AES and Base64

Chapter 1

1 What This Project Does

This project demonstrates a secure communication system between two ESP32 devices using MQTT protocole and:

  • AES-128: Fast and sufficient for many ESP32 use cases.
  • IV: Must be unique per message. Safe to send with ciphertext.
  • Padding: PKCS7 ensures variable-length plaintext is aligned to 16-byte blocks.
  • Key management: Avoid hardcoding for production, rotate keys periodically.

The subscriber receives an encrypted message in the format:

Base64( IV | Ciphertext )

Then

  1. Decodes Base64
  2. Splits the first 16 bytes as the IV
  3. Decrypts the remaining ciphertext using AES-CBC
  4. Removes PKCS7 padding
  5. Prints the plaintext
  6. Data Flow

Why/How

Publisher: Generates plaintext → encrypts with AES-CBC using a random IV → concatenates IV + ciphertext → Base64 encodes → publishes to MQTT.

Subscriber: Receives Base64 payload → decodes → splits IV and ciphertext → decrypts → prints plaintext.

Security: AES key is shared between publisher and subscriber; IV is public but must be unique for each message to ensure proper security.

Chapter 2

Libraries Used

Each library plays a critical role. Below are their purposes and alternatives.

2.1 <WiFi.h>

Purpose

Enables ESP32 to connect to Wi-Fi networks. It’s part of the ESP32 core and provides low-level networking functions.

2.2 <PubSubClient.h>

Purpose

A lightweight MQTT client library for Arduino. Handles:

  • Connecting to an MQTT broker
  • • Subscribing to topics
  • • Receiving messages via callback
  • • Publishing messages

2.3 <AESLib.h>

Purpose

Provides AES encryption and decryption in multiple modes (CBC, ECB, CTR). This project uses CBC mode with PKCS7 padding. It supports key sizes of 128, 192, and 256 bits.

Key Features

  • CBC mode requires an IV
  • • PKCS7 padding ensures block alignment
  • • IV is modified during encryption (so we copy it)

2.4 “libb64/cdecode.h”

Purpose

Efficient Base64 decoding for embedded systems. Converts ASCII Base64 strings back into raw binary data.

Why This Over Others?

  • Lightweight and memory-efficient
  • No dynamic allocation
  • Works well on ESP32 with small stack

Chapter 3

Full Source Code Listing

3.1 ESP32 MQTT AES-CBC Subscriber with Base64 Decode

#include <WiFi.h>
#include <PubSubClient.h>
#include <AESLib.h>
#include <libb64/cdecode.h>
const char* ssid = "ooredoo-6CCC29–5G";
const char* pass = "79B421EFWm35";
const char* mqtt_server = "broker.emqx.io";
const char* topic = "esp32pub";
WiFiClient espClient;
PubSubClient client(espClient);
// AES key must match publisher
AESLib aesLib;
byte aes_key[] = {
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
};
String decrypt_message(byte* ciphertext, uint16_t msgLen, byte* iv) {
byte decrypted[128];
uint16_t decLen = aesLib.decrypt(
ciphertext,
msgLen,
decrypted,
aes_key,
sizeof(aes_key),
iv
);
decrypted[decLen] = 0;
return String((char*)decrypted);
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("[arrived] ");
Serial.print(length);
Serial.println(" bytes");
// Base64 decode
byte decoded[128];
int decodedLength = base64_decode_chars((char*)payload, length, (char*)decoded);
if (decodedLength < 32 || (decodedLength - 16) % 16 != 0) {
Serial.println("Invalid message length");
return;
}
// Extract IV
byte iv[16];
memcpy(iv, decoded, 16);
// Extract ciphertext
uint16_t ciphertextLen = decodedLength - 16;
byte ciphertext[ciphertextLen];
memcpy(ciphertext, decoded + 16, ciphertextLen);
// Print IV
Serial.print("IV: ");
for (int i = 0; i < 16; i++) {
if (iv[i] < 0x10) Serial.print("0");
Serial.print(iv[i], HEX);
}
// Print Ciphertext
Serial.print(" | ");
for (int i = 0; i < ciphertextLen; i++) {
if (ciphertext[i] < 0x10) Serial.print("0");
Serial.print(ciphertext[i], HEX);
}
Serial.println();
// Copy IV for decryption (AES modifies it)
byte iv_copy[16];
memcpy(iv_copy, iv, 16);
// Decrypt
String decrypted = decrypt_message(ciphertext, ciphertextLen, iv_copy);
Serial.print("Decrypted: ");
Serial.println(decrypted);
}
void reconnect() {
while (!client.connected()) {
Serial.print("Connecting to MQTT…");
if (client.connect("ESP32Sub")) {
Serial.println("connected");
client.subscribe(topic);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" retry in 5s");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" connected");
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
aesLib.set_paddingmode(paddingMode::PKCS7); // PKCS7 padding
}
void loop() {
if (!client.connected()) reconnect();
client.loop();
}

Chapter 4

Line-by-Line Walkthrough

4.1 Includes

#include <WiFi.h> // ESP32 WiFi stack
#include <PubSubClient.h> // MQTT client (async)
#include <AESLib.h> // AES-CBC decrypt, PKCS7 padding
#include <libb64/cdecode.h> // Base64 decoder

Why/How

Why these? WiFi.h connects to the network. PubSubClient handles MQTT communication. AESLib performs decryption. libb64 safely converts Base64 to binary for decryption.

4.2 Configuration Constants

const char* ssid = "ooredoo-6CCC29–5G";
const char* pass = "79B421EFWm35";
const char* mqtt_server = "broker.emqx.io";
const char* topic = "esp32pub";

Tip: Replace these with your own Wi-Fi and MQTT settings. broker.emqx.io is a free public broker. For production, use TLS (port 8883).

4.3 Global Clients

WiFiClient espClient;
PubSubClient client(espClient);

Why/How

WiFiClient is used by PubSubClient to manage TCP connections. This is the standard pattern in Arduino MQTT.

4.4 AES Key Choice

AESLib aesLib;
byte aes_key[] = {
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
};

Note: This is the famous AES-128 test key from NIST FIPS 197. It’s 16 bytes (128 bits), perfect for AES-128. Both publisher and subscriber must use the same key. Never hardcode keys in production — use secure storage or key exchange.

4.5 Decryption Helper Function

String decrypt_message(byte* ciphertext, uint16_t msgLen, byte* iv) {
byte decrypted[128];
uint16_t decLen = aesLib.decrypt(
ciphertext, msgLen, decrypted,
aes_key, sizeof(aes_key), iv
);
decrypted[decLen] = 0;
return String((char*)decrypted);
}

Why/How: This function decrypts using AES-CBC. The IV is passed in (a copy), since AESLib modifies it. PKCS7 unpadding is applied automatically. The result is null-terminated and returned as a String.

4.6 MQTT Callback

void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("[arrived] ");
Serial.print(length);
Serial.println(" bytes");
// …
}

Tip: This callback is triggered whenever a message arrives on the subscribed topic.

Base64 decoding:

byte decoded[128];
int decodedLength = base64_decode_chars((char*)payload, length, (char*)decoded);

Why/How: Base64 decodes the MQTT payload into raw bytes. The output is stored in decoded.

Message validation:

if (decodedLength < 32 || (decodedLength - 16) % 16 != 0) {
Serial.println("Invalid message length");
return;
}

Note: Checks — At least 16 bytes (IV) + 16 bytes (ciphertext); Ciphertext must be multiple of 16 (AES block size).

byte iv[16]; memcpy(iv, decoded, 16);
uint16_t ciphertextLen = decodedLength - 16;
byte ciphertext[ciphertextLen];
memcpy(ciphertext, decoded + 16, ciphertextLen);

Why/How

Get Mohamed Amine Barhoumi’s stories in your inbox

Join Medium for free to get updates from this writer.

Splits the decoded data: first 16 bytes = IV, rest = ciphertext.

byte iv_copy[16]; memcpy(iv_copy, iv, 16);
String decrypted = decrypt_message(ciphertext, ciphertextLen, iv_copy);

Tip

AESLib modifies the IV during decryption, so we pass a copy.

4.7 Reconnect Loop

void reconnect() {
while (!client.connected()) {
if (client.connect("ESP32Sub")) {
client.subscribe(topic);
} else {
delay(5000);
}
}
}

Why/How

Ensures persistent connection. On failure, retries every 5 seconds. Consider using a unique client ID in production.

4.8 Setup and Loop

void setup() {
Serial.begin(115200);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) delay(500);
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
aesLib.set_paddingmode((paddingMode)1); // PKCS7
}

Note

(paddingMode)1 sets PKCS7 padding, which is required for CBC mode. Without it, decryption fails on non-block-sized messages.

void loop() {
if (!client.connected()) reconnect();
client.loop();
}

Why/How

client.loop() must be called continuously to process incoming messages and maintain the connection.

Chapter 5

Keys, IVs, Padding, and Security

5.1 Keys

• AES-128 (16B): Fast and secure for most embedded uses.
• AES-192/256: Higher security margin but slower.
• Key Management: Never hardcode in production. Use secure elements or key derivation.

5.2 IVs

• Random and unique per message.
• Not secret — can be sent with ciphertext.
• IV reuse with same key compromises security.

5.3 Padding

  • PKCS7: Adds 1–16 bytes so data fits AES blocks.
  • CTR mode: Stream cipher — no padding needed.
  • GCM: Authenticated encryption — includes integrity.

Chapter 6

ESP32 AES Publisher Code

This chapter complements the subscriber example by showing the full publisher code. It demonstrates how to generate a message, encrypt it using AES-CBC with a random IV, encode it in Base64, and publish it over MQTT.

6.1 Libraries

#include <WiFi.h>
#include <PubSubClient.h>
#include <AESLib.h>
#include <base64.h> // Arduino base64 library

Why/How

Why these? WiFi.h connects the ESP32 to the network, PubSubClient.h handles MQTT, AESLib.h encrypts messages, base64.h encodes the binary payload to text for MQTT.

6.2 Configuration and Buffers

const char* ssid = "ooredoo-6CCC29–5G";
const char* pass = "79B421EFWm35";
const char* mqtt_server = "broker.emqx.io";
const char* topic = "esp32/pub";
WiFiClient espClient;
PubSubClient client(espClient);
AESLib aesLib;
byte aes_key[] = { /* same 16-byte key as subscriber */ };
byte aes_iv[16];
byte ciphertext[128];

Notes: The key must match the subscriber. IV and ciphertext buffers hold per-message encryption data.

6.3 Setup Function

void setup() {
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
aesLib.set_paddingmode(PKCS7); // PKCS7 padding for AES-CBC
randomSeed(analogRead(0)); // Seed RNG for IV generation
}

Tip

randomSeed(analogRead(0)) uses analog noise (floating pin) to generate unpredictable IVs.

6.3.1 PKCS7 Padding Explained

aesLib.set_paddingmode((paddingMode)1); // PKCS7 padding

Why/How

AES is a block cipher with a fixed block size (16 bytes). PKCS7 padding ensures that every plaintext fits exactly into a multiple of 16 bytes.

How it works:

  • Suppose the plaintext is shorter than a multiple of 16 bytes.
  • Count how many bytes are needed to reach 16-byte boundary (let’s say n bytes).
  • Append n bytes, each with the value n.
    — Example: plaintext = “HELLO” (5 bytes) → need 11 bytes padding → append 0x0B eleven times
  • If plaintext is exactly 16 bytes, append a full block of padding: 16 bytes of value 0x10.
  • After AES-CBC encryption, the padding is hidden inside the ciphertext.

Decryption:

  • The subscriber decrypts the ciphertext using AES-CBC.
  • AESLib knows the padding mode is PKCS7.
  • It automatically removes the last n bytes based on the PKCS7 scheme.
  • Result: original plaintext without padding is recovered.

Tip

Key point: The padding is never visible in the transmitted ciphertext. Both publisher and subscriber must agree on using PKCS7, otherwise decryption fails or returns garbage.

6.4 Main Loop Explained

The loop() function continuously runs, ensuring the device stays connected, generates messages, encrypts them, and publishes securely.

void loop() {
if (!client.connected()) reconnect(); // Ensure MQTT is connected
client.loop(); // Process MQTT packets
String message = "Hello from publisher at " + String(millis());
// Generate random IV
for (int i = 0; i < 16; i++) {
aes_iv[i] = random(0, 256);
}
byte iv_copy[16];
memcpy(iv_copy, aes_iv, 16); // AES modifies IV internally
uint16_t encLen = aesLib.encrypt(
(byte*)message.c_str(),
message.length(),
ciphertext,
aes_key,
sizeof(aes_key),
iv_copy
);
byte payload[16 + encLen];
memcpy(payload, aes_iv, 16); // Prepend IV
memcpy(payload + 16, ciphertext, encLen); // Append ciphertext
String encoded = base64::encode(payload, sizeof(payload));
client.publish(topic, encoded.c_str());
delay(5000); // Wait 5s before next message
}

Step-by-Step Explanation

  • Connection check: if (!client.connected()) reconnect(); ensures MQTT stays connected.
  • MQTT loop handler: client.loop(); processes incoming packets and keeps connection alive.
  • Message generation: String message = “Hello …” + millis(); creates a unique message every iteration.
  • Random IV creation: for (int i=0; i<16; i++) aes_iv[i] = random(0,256); creates a fresh 16-byte IV for AES-CBC.
  • IV copy for encryption: memcpy(iv_copy, aes_iv, 16); preserves the original IV, because AES-CBC modifies it.
  • AES encryption: aesLib.encrypt(…); encrypts the message with the shared key + IV.
  • Payload construction:
    — Prepend the IV: memcpy(payload, aes_iv, 16);
    — Append ciphertext: memcpy(payload+16, ciphertext, encLen);
  • Base64 encoding: String encoded = base64::encode(payload,…); converts binary payload to text.
  • MQTT publish: client.publish(topic, encoded.c_str()); sends encrypted message to the broker.
  • Delay: delay(5000); pauses 5 seconds before the next iteration.

Why/How

Each cycle: Check connection → Generate message → Encrypt → Prepend IV → Encode → Publish → Wait.

Chapter 7

Extra: Generating AES Keys on the ESP32

Sometimes you may want the ESP32 to generate its own AES key rather than using a pre-defined one. This ensures randomness and uniqueness per device. The ESP32 has a hardware random number generator that we can use.

Example: Generate a 16-byte AES-128 Key

Listing 7.1: Generate AES Key on ESP32

#include <Arduino.h>
byte aes_key[16];
void setup() {
Serial.begin(115200);
// Fill aes_key with random bytes
for (int i = 0; i < 16; i++) {
aes_key[i] = (byte)(esp_random() & 0xFF); // only keep 1 byte
}
// Print key in 0x.. format
Serial.println("AES Key:");
for (int i = 0; i < 16; i++) {
Serial.print("0x");
if (aes_key[i] < 0x10) Serial.print("0");
Serial.print(aes_key[i], HEX);
if (i < 15) Serial.print(", ");
}
Serial.println();
}
void loop() {
}

Why/How

What it does:
• Uses esp_random() to generate a 32-bit random number.
• Keeps only the lowest byte (0xFF mask) for each of the 16 bytes.
• Prints the key in {0x..} hexadecimal format suitable for Arduino sketches.

Notes:
• This key is random at each reset.
• Both publisher and subscriber must use the same key for AES encryption/decryption.
• AES-128 uses 16 bytes; you can also generate 24 or 32 bytes for AES-192 or AES-256.

Tip: You can generate the key once, print it, and then copy it into your subscriber sketch to use. This ensures both sides have the same key while keeping the initial generation secure.

You can also use this key too, recently generated:

0xD8, 0x45, 0x21, 0x0D, 0xE4, 0x59, 0x65, 0xE9, 0x26, 0x86, 0x8B, 0x55, 0xDE, 0xDB, 0xA0, 0xE7

Chapter 8

MQTT Details and Options

• Broker: broker.emqx.io:1883 — free public broker.
• QoS: 0 (fire-and-forget), 1 (at least once), 2 (exactly once).
• Client ID: Should be unique. Use ESP.getChipId().
• TLS: Use WiFiClientSecure and port 8883 for encryption.
• Keepalive: Default 15s — broker disconnects idle clients.

--

--

0 followers

Engineering student at ESIEA | Computer vision and embedded AI | Building on-device models for connected beehives (BeeSafe) | ESP32, YOLO, ONNX

See more from me