Introducción
La seguridad en dispositivos IoT es un aspecto crítico que a menudo se pasa por alto en el desarrollo de prototipos. En esta lección aprenderemos las mejores prácticas para el manejo seguro de credenciales y llaves privadas en ESP32, específicamente en el contexto de un sistema completo que incluye sensores DHT11, comunicación MQTT con Mosquitto y una aplicación Flask.
El manejo inadecuado de credenciales puede comprometer toda la infraestructura IoT, desde el acceso a redes WiFi hasta la autenticación en brokers MQTT y APIs. Exploraremos técnicas de almacenamiento seguro, cifrado y gestión de credenciales que son fundamentales para sistemas en producción.
Conceptos Fundamentales
La gestión segura de credenciales en ESP32 involucra múltiples capas de seguridad y diferentes tipos de información sensible que debemos proteger:
Tipos de Credenciales en nuestro Stack
- Credenciales WiFi: SSID y contraseña de red
- Credenciales MQTT: Usuario, contraseña, certificados TLS
- Claves API: Tokens para comunicación con Flask
- Certificados: Certificados CA, cliente, llaves privadas
Métodos de Almacenamiento Seguro
- NVS (Non-Volatile Storage): Sistema de almacenamiento clave-valor del ESP32
- SPIFFS/LittleFS: Sistema de archivos para datos estructurados
- Particiones cifradas: Almacenamiento con cifrado por hardware
- Secure Boot: Verificación de integridad del firmware
Principios de Seguridad
- Separación de código y configuración: Nunca hardcodear credenciales
- Cifrado en reposo: Datos cifrados cuando no están en uso
- Cifrado en tránsito: TLS/SSL para comunicaciones
- Rotación de credenciales: Cambio periódico de llaves y contraseñas
- Principio de menor privilegio: Acceso mínimo necesario
Implementación Práctica
Implementaremos un sistema completo de gestión de credenciales que incluye almacenamiento seguro, cifrado y comunicación autenticada entre ESP32, Mosquitto y Flask.
1. Configuración de Credenciales Seguras - credentials.h
#ifndef CREDENTIALS_H
#define CREDENTIALS_H
// Estructura para credenciales WiFi
struct WiFiCredentials {
char ssid[32];
char password[64];
};
// Estructura para credenciales MQTT
struct MQTTCredentials {
char broker[64];
int port;
char username[32];
char password[64];
char client_id[32];
char ca_cert[2048];
char client_cert[2048];
char private_key[2048];
};
// Estructura para configuración del dispositivo
struct DeviceConfig {
char device_id[16];
char api_key[64];
int publish_interval;
bool tls_enabled;
};
#endif
2. Gestor de Credenciales con NVS - CredentialManager.cpp
#include <Preferences.h>
#include <mbedtls/aes.h>
#include "credentials.h"
class CredentialManager {
private:
Preferences preferences;
uint8_t encryption_key[32] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c,
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};
public:
bool initialize() {
return preferences.begin("secure_creds", false);
}
bool storeWiFiCredentials(const WiFiCredentials& creds) {
preferences.putString("wifi_ssid", creds.ssid);
// Cifrar contraseña antes de almacenar
String encrypted_password = encryptString(creds.password);
preferences.putString("wifi_pass", encrypted_password);
Serial.println("Credenciales WiFi almacenadas de forma segura");
return true;
}
bool loadWiFiCredentials(WiFiCredentials& creds) {
String ssid = preferences.getString("wifi_ssid", "");
String encrypted_password = preferences.getString("wifi_pass", "");
if (ssid.length() == 0 || encrypted_password.length() == 0) {
Serial.println("Error: No se encontraron credenciales WiFi");
return false;
}
strcpy(creds.ssid, ssid.c_str());
String decrypted_password = decryptString(encrypted_password);
strcpy(creds.password, decrypted_password.c_str());
return true;
}
bool storeMQTTCredentials(const MQTTCredentials& creds) {
preferences.putString("mqtt_broker", creds.broker);
preferences.putInt("mqtt_port", creds.port);
preferences.putString("mqtt_user", creds.username);
// Cifrar información sensible
preferences.putString("mqtt_pass", encryptString(creds.password));
preferences.putString("mqtt_client", creds.client_id);
preferences.putString("ca_cert", creds.ca_cert);
preferences.putString("client_cert", creds.client_cert);
preferences.putString("private_key", encryptString(creds.private_key));
Serial.println("Credenciales MQTT almacenadas de forma segura");
return true;
}
bool loadMQTTCredentials(MQTTCredentials& creds) {
String broker = preferences.getString("mqtt_broker", "");
if (broker.length() == 0) {
Serial.println("Error: No se encontraron credenciales MQTT");
return false;
}
strcpy(creds.broker, broker.c_str());
creds.port = preferences.getInt("mqtt_port", 1883);
strcpy(creds.username, preferences.getString("mqtt_user", "").c_str());
strcpy(creds.password, decryptString(preferences.getString("mqtt_pass", "")).c_str());
strcpy(creds.client_id, preferences.getString("mqtt_client", "").c_str());
strcpy(creds.ca_cert, preferences.getString("ca_cert", "").c_str());
strcpy(creds.client_cert, preferences.getString("client_cert", "").c_str());
strcpy(creds.private_key, decryptString(preferences.getString("private_key", "")).c_str());
return true;
}
private:
String encryptString(const String& plaintext) {
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, encryption_key, 256);
size_t len = ((plaintext.length() / 16) + 1) * 16;
uint8_t* encrypted = new uint8_t[len];
uint8_t* padded_input = new uint8_t[len];
memset(padded_input, 0, len);
memcpy(padded_input, plaintext.c_str(), plaintext.length());
for (size_t i = 0; i < len; i += 16) {
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT,
padded_input + i, encrypted + i);
}
String result = base64_encode(encrypted, len);
delete[] encrypted;
delete[] padded_input;
mbedtls_aes_free(&aes);
return result;
}
String decryptString(const String& encrypted) {
size_t decoded_len = base64_decode_length(encrypted);
uint8_t* decoded = new uint8_t[decoded_len];
base64_decode(encrypted, decoded);
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, encryption_key, 256);
uint8_t* decrypted = new uint8_t[decoded_len];
for (size_t i = 0; i < decoded_len; i += 16) {
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT,
decoded + i, decrypted + i);
}
String result = String((char*)decrypted);
delete[] decoded;
delete[] decrypted;
mbedtls_aes_free(&aes);
return result;
}
String base64_encode(const uint8_t* data, size_t len) {
const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
String result;
result.reserve(((len + 2) / 3) * 4);
for (size_t i = 0; i < len; i += 3) {
uint32_t tmp = data[i] << 16;
if (i + 1 < len) tmp |= data[i + 1] << 8;
if (i + 2 < len) tmp |= data[i + 2];
result += chars[(tmp >> 18) & 0x3F];
result += chars[(tmp >> 12) & 0x3F];
result += (i + 1 < len) ? chars[(tmp >> 6) & 0x3F] : '=';
result += (i + 2 < len) ? chars[tmp & 0x3F] : '=';
}
return result;
}
size_t base64_decode_length(const String& encoded) {
return (encoded.length() * 3) / 4;
}
void base64_decode(const String& encoded, uint8_t* output) {
// Implementación básica de decodificación base64
// En producción usar una librería robusta
const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int val = 0, bits = -8;
size_t index = 0;
for (char c : encoded) {
if (c == '=') break;
const char* pos = strchr(chars, c);
if (!pos) continue;
val = (val << 6) + (pos - chars);
bits += 6;
if (bits >= 0) {
output[index++] = (val >> bits) & 0xFF;
bits -= 8;
}
}
}
};
3. Cliente MQTT Seguro - SecureMQTTClient.cpp
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>
#include "CredentialManager.cpp"
#define DHT_PIN 4
#define DHT_TYPE DHT11
class SecureMQTTClient {
private:
WiFiClientSecure wifiClientSecure;
PubSubClient mqttClient;
DHT dht;
CredentialManager credManager;
MQTTCredentials mqttCreds;
DeviceConfig deviceConfig;
unsigned long lastPublish = 0;
public:
SecureMQTTClient() : mqttClient(wifiClientSecure), dht(DHT_PIN, DHT_TYPE) {}
bool initialize() {
Serial.begin(115200);
dht.begin();
if (!credManager.initialize()) {
Serial.println("Error: No se pudo inicializar el gestor de credenciales");
return false;
}
// Configurar dispositivo (normalmente se haría en setup inicial)
setupInitialCredentials();
if (!credManager.loadMQTTCredentials(mqttCreds)) {
Serial.println("Error: No se pudieron cargar credenciales MQTT");
return false;
}
return connectToWiFi() && connectToMQTT();
}
void loop() {
if (!mqttClient.connected()) {
if (!connectToMQTT()) {
delay(5000);
return;
}
}
mqttClient.loop();
// Publicar datos del sensor cada intervalo configurado
unsigned long now = millis();
if (now - lastPublish >= (deviceConfig.publish_interval * 1000)) {
publishSensorData();
lastPublish = now;
}
}
private:
bool connectToWiFi() {
// TODO: Reemplaza con tus credenciales WiFi reales
// WiFi.begin("TU_SSID", "TU_PASSWORD");
// Espera la conexión y devuelve true/false según corresponda
return true; // Demo
}
bool connectToMQTT() {
// TODO: Configura broker/puerto/seguridad TLS y callback
// mqttClient.setServer("tu_broker", 8883);
// mqttClient.setCallback(onMessage);
if (mqttClient.connected()) return true;
return mqttClient.connect("ESP32_SECURE"); // Demo sin usuario/clave
}
void publishSensorData() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) {
return;
}
StaticJsonDocument<200> doc;
doc["temperature"] = t;
doc["humidity"] = h;
char buffer[128];
size_t n = serializeJson(doc, buffer, sizeof(buffer));
mqttClient.publish("home/sensors/data", buffer, n);
}
void setupInitialCredentials() {
// TODO: Guarda credenciales iniciales en Secure Storage si aplica
// credManager.saveMQTTCredentials({"broker", 8883, "user", "pass"});
deviceConfig.publish_interval = 30; // segundos
}
};