Manejo de credenciales y llaves privadas en ESP32

Almacenamiento seguro de credenciales, mejores prácticas de seguridad

Módulo 6 ⏱️ 2 horas 🛠️ ESP32 + DHT11 🌐 Flask + MQTT

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
    }
};