The most reliable way to achieve ESP32 simple text encryption without pulling in heavy external libraries is a rolling XOR cipher paired with hexadecimal encoding. While the ESP32 features a dedicated hardware cryptographic accelerator for AES, a software-based XOR stream cipher is vastly superior for low-latency serial debugging, basic obfuscation, and educational projects where minimizing memory footprint is critical.
In this guide, we will build a bidirectional encrypted serial bridge. Plain text typed into the serial monitor is encrypted and output as hex; hex strings prefixed with a tilde (~) are decrypted back to plain text. The code targets the ubiquitous ESP32-WROOM-32 (30-pin DevKit V1) and requires zero external dependencies.
Project Scope and Hardware BOM
Before writing code, verify your hardware. The ESP32's dual-core architecture and 520KB of SRAM make it overkill for a simple XOR cipher, but this project serves as a foundational template for secure IoT sensor payloads.
- Microcontroller: ESP32-WROOM-32 DevKit V1 (30-pin variant). Note: The 38-pin ESP32-WROVER will also work, but pin mappings for onboard LEDs differ.
- Connection: High-quality USB 2.0 Micro-B data cable (avoid charge-only cables, which cause silent serial drops).
- Indicator: 5mm LED (any color) and a 220Ω through-hole resistor (optional, for external status indication if the onboard GPIO 2 LED is insufficient).
- Software: Arduino IDE 2.x with the official Espressif ESP32 Board Package (v2.0.14 or newer recommended for stable
mbedtlsintegration if you expand later).
Algorithm Showdown: Which Encryption Fits Your ESP32?
Choosing the right algorithm depends on your payload size and security requirements. Below is a data-dense comparison of common text encryption methods running natively on the ESP32-WROOM-32 clocked at 240MHz.
| Algorithm | Execution Time (per 1KB) | RAM Overhead | Security Level | Library Dependency |
|---|---|---|---|---|
| Rolling XOR (This Build) | ~18 µs | 16 bytes (Key) | Low (Obfuscation) | None (Pure C++) |
| AES-128-CBC (Software) | ~450 µs | ~2.5 KB | High | AESLib / Crypto |
| AES-128-CBC (Hardware) | ~35 µs | ~1.2 KB | High | ESP-IDF mbedtls |
| ChaCha20 | ~120 µs | ~1.5 KB | Very High | Arduino CryptoLib |
Pin Mapping and Wiring
This project is primarily serial-based, but we map the onboard LED to provide visual feedback when the encryption buffer is actively processing data. This prevents "ghost" resets from looking like successful transmissions.
| Component | ESP32 Pin | Direction | Notes |
|---|---|---|---|
| Onboard Status LED | GPIO 2 | OUTPUT | Active HIGH on most DevKit V1 clones. |
| External LED Anode | GPIO 25 (Optional) | OUTPUT | Use if GPIO 2 is occupied by SDIO. |
| USB Serial TX/RX | GPIO 1 / GPIO 3 | I/O | Routed through the CP2102/CH340 USB bridge. |
The Code: Rolling XOR with Hex Encoding
The following C++ code is fully compilable in the Arduino IDE. It uses a 16-byte pre-shared key (PSK) to perform a rolling XOR operation. Because raw binary data cannot be safely printed to the Arduino Serial Monitor, the encrypted bytes are encoded into a hexadecimal string.
#include <Arduino.h>
// --- PIN DEFINITIONS ---
#define STATUS_LED 2
// --- CRYPTO CONFIGURATION ---
#define KEY_LENGTH 16
#define MAX_BUFFER 256
// 16-byte Pre-Shared Key (PSK). Change this to your own random bytes.
const uint8_t encryptionKey[KEY_LENGTH] = {
0x3F, 0xA1, 0x2B, 0x9C, 0x4D, 0xE5, 0x77, 0x12,
0x88, 0x6F, 0xC3, 0x5A, 0xD9, 0x04, 0xB7, 0x2E
};
char inputBuffer[MAX_BUFFER];
int bufferIndex = 0;
// Helper: Convert a single hex character to its integer value
uint8_t hexCharToByte(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return 0;
}
void setup() {
Serial.begin(115200);
pinMode(STATUS_LED, OUTPUT);
// Prevent Serial.readStringUntil from blocking indefinitely
Serial.setTimeout(5);
delay(1000);
Serial.println("\n--- ESP32 Simple Text Encryption Bridge ---");
Serial.println("Type plain text to encrypt.");
Serial.println("Type ~ followed by HEX to decrypt (e.g., ~5A4B...)");
}
void loop() {
while (Serial.available()) {
char c = Serial.read();
// Blink LED to indicate serial activity
digitalWrite(STATUS_LED, HIGH);
if (c == '\n' || c == '\r') {
if (bufferIndex > 0) {
inputBuffer[bufferIndex] = '\0'; // Null-terminate
processInput(inputBuffer);
bufferIndex = 0;
}
} else {
// Prevent buffer overflow
if (bufferIndex < MAX_BUFFER - 1) {
inputBuffer[bufferIndex++] = c;
} else {
Serial.println("\n[ERROR] Buffer overflow. Input truncated.");
bufferIndex = 0;
}
}
digitalWrite(STATUS_LED, LOW);
}
}
void processInput(char* data) {
if (data[0] == '~') {
// DECRYPT MODE: Hex string to Plain text
decryptHexToText(data + 1);
} else {
// ENCRYPT MODE: Plain text to Hex string
encryptTextToHex(data);
}
}
void encryptTextToHex(const char* plaintext) {
Serial.print("[TX ENCRYPTED]: ");
int len = strlen(plaintext);
for (int i = 0; i < len; i++) {
uint8_t encryptedByte = plaintext[i] ^ encryptionKey[i % KEY_LENGTH];
if (encryptedByte < 0x10) Serial.print('0'); // Pad single digit hex
Serial.print(encryptedByte, HEX);
}
Serial.println();
}
void decryptHexToText(const char* hexString) {
int len = strlen(hexString);
if (len % 2 != 0) {
Serial.println("[ERROR] Invalid hex length (must be even).");
return;
}
Serial.print("[RX DECRYPTED]: ");
for (int i = 0; i < len; i += 2) {
uint8_t highNibble = hexCharToByte(hexString[i]);
uint8_t lowNibble = hexCharToByte(hexString[i+1]);
uint8_t encryptedByte = (highNibble << 4) | lowNibble;
// XOR back to plaintext
char decryptedChar = encryptedByte ^ encryptionKey[(i/2) % KEY_LENGTH];
Serial.print(decryptedChar);
}
Serial.println();
}
Debugging: When the Cipher Breaks
Cryptographic implementations on microcontrollers frequently fail due to memory mismanagement rather than math errors. If your ESP32 resets or outputs garbage, review these exact failure modes.
Exact Error Strings and Ranked Causes
Error 1: Guru Meditation Error: Core 1 panic'ed (StackOverflow). exception was thrown: 0x3f400000
- Cause A (Most Likely): You attempted to upgrade the code to AES and allocated a large encryption buffer (e.g.,
uint8_t cipher[2048]) locally inside theloop()function. The ESP32's task stack is typically 8KB; large local arrays smash the stack. - Fix: Move large buffers to the global scope, or allocate them on the heap using
malloc()or theStringclass. The provided XOR code uses a safe 256-byte global buffer.
Error 2: fatal error: mbedtls/aes.h: No such file or directory
- Cause A: You tried to copy an ESP-IDF native AES example into the Arduino IDE. The Arduino core wraps
mbedtlsdifferently, and direct header inclusion often fails depending on your core version. - Fix: Install the
AESLibvia the Arduino Library Manager, or use the pure C++ XOR implementation above which bypasses the need for crypto headers entirely.
The First Three Things to Check
- Serial Monitor Line Endings: If the ESP32 seems to ignore your input or prints empty brackets, your Arduino Serial Monitor is likely set to "No Line Ending". Change the dropdown in the bottom right corner to Both NL & CR. The code relies on
\nor\rto trigger the encryption function. - Baud Rate Mismatch: Ensure both the code (
Serial.begin(115200)) and your Serial Monitor are set to 115200. A mismatch will result in high-entropy garbage that looks exactly like a broken cipher. - Key Synchronization: If you are testing this between two separate ESP32 boards, verify that the
encryptionKeyarray is byte-for-byte identical on both devices. A single flipped bit in the PSK will corrupt the entire decrypted stream.
Extending and Simplifying the Build
Depending on your end goal, you may need to scale this project up for production or down for a basic classroom demonstration.
How to Extend to Hardware AES
If you need to meet OWASP cryptographic storage standards for an IoT product, XOR is insufficient. To extend this build:
- Install the
AESLibby intrbiz via the Library Manager. - Replace the XOR loop with
aes128_cbc_enc(). - Critical Step: AES requires padding (usually PKCS7) to fill the final 16-byte block. You must append padding bytes before encryption and strip them after decryption, which will increase your payload size by up to 16 bytes.
How to Simplify for Education
If you are teaching basic logic gates or introductory programming, you can simplify the rolling XOR to a static Caesar Cipher (shift cipher). Replace the XOR line in the code with:
char encryptedChar = plaintext[i] + 3; // Shift ASCII value by 3
Warning: While easier to explain to beginners, a Caesar shift leaks the exact frequency distribution of the English language, making it trivially crackable via statistical analysis. It should only be used for educational demonstrations, never for actual data protection.






