Implement Encryption on Microcontrollers
Difficulty Level: Advanced
In this tutorial, you’ll learn how to implement encryption on resource-constrained microcontrollers to secure data in IoT, embedded systems, and wireless communication. We’ll cover algorithm selection, library integration, performance optimization, secure key management, and real-world considerations like power efficiency and attack resistance.
Why Encryption Matters for Microcontrollers
Microcontrollers often handle sensitive data—think medical devices transmitting patient vitals, industrial sensors reporting operational states, or smart locks managing access credentials. Without encryption, adversaries can:
- Eavesdrop on communication interfaces like UART, SPI, I2C, or wireless protocols (Wi-Fi, BLE, LoRa).
- Extract plaintext credentials from unsecured flash or RAM via physical access (e.g., JTAG debugging).
- Replay intercepted messages to spoof devices.
Recent incidents—like the 2021 Verkada camera hack or Mirai botnet attacks—demonstrate that unencrypted IoT devices are prime targets. Encryption ensures confidentiality, integrity, and authenticity, even on devices with limited CPU, memory, and power.
Types of Encryption Algorithms
Symmetric Encryption (AES)
- Advantages: Fast, low memory footprint, ideal for encrypting bulk data.
- Modes:
- ECB (Electronic Codebook): Simple but insecure—repeating plaintext blocks produce identical ciphertext, leaking patterns. Avoid unless data is truly random.
- CBC (Cipher Block Chaining): Chains blocks with an Initialization Vector (IV), preventing pattern leakage. Requires careful IV management.
- CTR (Counter Mode): Turns AES into a stream cipher, great for real-time data or partial encryption.
- Key Sizes: 128-bit (minimum for modern security), 192-bit, or 256-bit (highest security, but slower).
Asymmetric Encryption (RSA, ECC)
- Advantages: Enables secure key exchange and digital signatures without pre-shared secrets.
- Drawbacks: Computationally expensive—RSA key generation or decryption can take seconds on a microcontroller.
- Use Cases:
- RSA: Encrypt a symmetric key (e.g., AES) for secure exchange.
- ECC (Elliptic Curve Cryptography): Lighter than RSA. Use ECDH for key agreement, ECDSA for signatures.
- Example Workflow: ECDH negotiates a shared secret, then AES encrypts the data.
Hybrid Approach
Combine both: Use ECC/RSA for key exchange, then AES for efficient data encryption.
Components Required
- Microcontroller:
- ESP32: Dual-core, hardware AES acceleration, built-in Wi-Fi/BLE.
- STM32: Cryptographic hardware (AES, SHA, RNG) on select models (e.g., STM32F4/F7).
- Arduino Uno: No hardware crypto—software-only, slower but viable for prototyping.
- Cryptographic Library:
- Arduino: “Cryptography” library (simple AES-256 implementation).
- ESP32: mbed TLS (robust, supports TLS/SSL).
- STM32: STM32 Cryptographic Library (optimized for HAL).
- Secure Storage:
- External EEPROM: AT24C256 (encrypt data before storage).
- Secure Element: ATECC608A (hardware key storage, tamper-resistant).
- Communication Module:
- NRF24L01: Low-cost 2.4GHz wireless.
- LoRa: Long-range, low-power.
- ESP32 Wi-Fi: For internet-connected devices.
Step 1: Choosing a Library
Evaluate based on:
- RAM/Flash Usage: Tiny AES in C uses ~1.5KB flash, while mbed TLS can exceed 50KB with TLS support.
- Hardware Acceleration: ESP32’s AES-NI or STM32’s CRYP peripheral can yield 10x performance gains.
- Protocol Support: mbed TLS offers TLS/SSL for secure networking; simpler libraries don’t.
- Community Support: Check for recent updates and vulnerability patches.
Example 1: AES-128-CBC on ESP32 with mbed TLS
#include "mbedtls/aes.h"
mbedtls_aes_context aes;
uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; // Example key
uint8_t iv[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; // Random IV
uint8_t plaintext[64] = "Sensitive data to encrypt";
uint8_t ciphertext[64];
void setup() {
Serial.begin(115200);
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, key, 128);
// Encrypt with CBC mode
mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, sizeof(plaintext), iv, plaintext, ciphertext);
Serial.print("Ciphertext: ");
for (int i = 0; i < 64; i++) Serial.printf("%02x", ciphertext[i]);
Serial.println();
// Reset IV for decryption (in practice, reuse the same IV sent with ciphertext)
uint8_t iv_copy[16];
memcpy(iv_copy, iv, 16);
mbedtls_aes_setkey_dec(&aes, key, 128);
mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, sizeof(ciphertext), iv_copy, ciphertext, plaintext);
Serial.print("Decrypted: ");
Serial.println((char*)plaintext);
}
void loop() {}
Key Improvements Over ECB:
- IV Usage: Random IV per encryption ensures identical plaintexts yield different ciphertexts.
- Transmission: Send IV || ciphertext
(prepend IV to ciphertext) so the recipient can decrypt.
- Padding: mbed TLS handles PKCS#7 padding automatically for non-16-byte-aligned data.
Pitfalls:
- Reusing an IV with the same key compromises security—generate a new IV (e.g., from a hardware RNG) each time.
Example 2: RSA Key Exchange on STM32
#include "stm32_crypt.h"
RSA_Context_t rsa;
AES_Context_t aes;
uint8_t aes_key[16];
void secure_session_init() {
RSA_Init(&rsa);
RSA_GenerateKeyPair(&rsa, 2048); // 2048-bit key—takes ~5-10s on STM32F4!
// Transmit public key to peer (e.g., over UART or wireless)
send_data(rsa.public_key, sizeof(rsa.public_key));
// Receive encrypted AES key from peer
uint8_t encrypted_aes_key[256]; // RSA-2048 output size
receive_data(encrypted_aes_key, sizeof(encrypted_aes_key));
// Decrypt AES key with private key
RSA_Decrypt(&rsa, encrypted_aes_key, aes_key);
AES_Init(&aes, aes_key, AES_KEY_128);
}
Notes:
- RSA is slow on microcontrollers—use sparingly (e.g., once per session).
- For faster key exchange, consider ECDH (Elliptic Curve Diffie-Hellman).
Example 3: Lightweight ECC with Microcrypt
For resource-constrained devices like Arduino:
#include "micro_ecc.h"
uint8_t private_key[32];
uint8_t public_key[64];
uint8_t shared_secret[32];
void setup() {
ecc_generate_keypair(private_key, public_key);
// Exchange public_key with peer, receive their public_key
ecc_compute_shared_secret(private_key, peer_public_key, shared_secret);
// Use shared_secret as AES key
}
- Pros: ECC uses smaller keys (256-bit ECC ≈ 3072-bit RSA) and is faster.
- Cons: Library support varies; Microcrypt lacks TLS.
Key Management Best Practices
1. Never Hardcode Keys:
- Hardcoded keys in firmware are easily extracted via reverse engineering.
- Inject keys during manufacturing or derive them with a KDF (e.g., HKDF from a master secret).
2. Secure Storage:
- Secure Element: ATECC608A stores keys in tamper-proof hardware:
#include "ATECC608.h"
uint8_t key[16];
atecc608.read_slot(0, key, AES_KEY_SIZE); // Retrieve key securely
- Encrypted EEPROM: Encrypt keys with a master key before storing in AT24C256.3. Key Rotation:
- Update keys periodically via TLS handshake or a secure out-of-band channel (e.g., physical provisioning).
4. Random Number Generation:
- Use hardware RNG (e.g., ESP32’s TRNG) for IVs and keys—software PRNGs are predictable.
Performance Optimization
1. Hardware Acceleration:
- ESP32: Enable AES-NI via menuconfig
in esp-idf (CONFIG_MBEDTLS_HARDWARE_AES=y
).
- STM32: Use CRYP peripheral for AES and HASH peripheral for SHA.
2. Precompute Keys:
- Initialize AES context once and reuse it for multiple blocks.
3. Avoid Dynamic Allocation:
- Heap fragmentation crashes MCUs—use stack or static buffers:
uint8_t buffer[16] __attribute__((aligned(16))); // 16-byte aligned for AES
4. Low-Power Tweaks:- Use CTR mode for stream encryption to minimize wake-up cycles in sleep-heavy IoT devices.
Security Considerations
1. Side-Channel Attacks:
- Power Analysis: Avoid branches or loops that depend on secret data—use constant-time operations.
- Timing Attacks: Ensure crypto libraries (e.g., mbed TLS) use timing-safe comparisons.
2. Firmware Signing:
- Use ECDSA to sign updates, preventing malicious code injection.
3. Secure Boot:
- Enable MCU secure boot (e.g., ESP32’s flash encryption) to run only signed firmware.
4. Physical Tamper Resistance:
- Disable JTAG/SWD after deployment to block memory dumping.
Troubleshooting
- Garbage Output:
- Check IV reuse (must match encryption IV) or padding (use PKCS#7).
- Verify key length matches algorithm (e.g., 16 bytes for AES-128).
- Memory Overflows:
- Ensure buffers are multiples of block size (16 bytes for AES).
- Use sizeof()
correctly in crypto calls.
- Slow Performance:
- Profile with a logic analyzer—switch to hardware acceleration if available.
- Test Vectors:
- Validate with NIST AES Known Answer Tests or RFC test cases (e.g., RFC 3686 for CTR).
Real-World Application: Secure IoT Sensor
Imagine an ESP32-based temperature sensor sending data over LoRa:
1. Generate an ECC keypair on boot.
2. Exchange public keys with a gateway using ECDH.
3. Derive a shared AES key.
4. Encrypt temperature readings with AES-CTR and transmit.
5. Rotate keys daily via a secure TLS handshake with the gateway.
Conclusion
Encryption on microcontrollers is a balancing act between security, performance, and resource constraints. Prioritize symmetric encryption (AES) with hardware acceleration, secure key storage (e.g., ATECC608A), and TLS for networked devices. Regularly audit libraries for vulnerabilities and test against real-world attack scenarios.
FAQ
- Can I use AES on an Arduino Uno without hardware acceleration?
- Yes, but it’s slower (e.g., ~1ms per 16-byte block). Use lightweight libraries like “Cryptography” and stick to AES-128 for better performance.
- How do I generate a secure IV?
- Use a hardware random number generator (e.g., ESP32’s TRNG) or a cryptographically secure PRNG seeded with entropy from ADC noise or timing jitter.
- Is RSA practical on microcontrollers?
- Not for frequent use—RSA key generation or decryption takes seconds. Use ECC instead for faster asymmetric crypto.
- What’s the best way to store keys long-term?
- Use a Secure Element like ATECC608A for tamper-proof storage. Avoid flash unless encrypted with a master key.
- How do I test my encryption implementation?
- Compare outputs against NIST test vectors, fuzz inputs for edge cases, and use a logic analyzer to check timing and memory usage.