#include "secure_layer_manager.h"
#include "secure_utils.h"
#include "device_static_key.h"
#include "mbedtls/ecdh.h"
#include "mbedtls/gcm.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/hkdf.h"
#include "mbedtls/md.h"
#include
SecureLayerManager& SecureLayerManager::getInstance() {
static SecureLayerManager instance;
return instance;
}
SecureLayerManager::SecureLayerManager()
: initialized(false), sessionTimeout(SECURE_SESSION_TIMEOUT) {
}
SecureLayerManager::~SecureLayerManager() {
end();
}
bool SecureLayerManager::begin() {
if (initialized) return true;
LOG_INFO("SecureLayerManager", "Initializing secure layer...");
// Initialize entropy and PRNG
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
mbedtls_ecdh_init(&ecdh_context);
const char* pers = "esp32_secure_layer_v1";
int ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
(const unsigned char*)pers, strlen(pers));
if (ret != 0) {
LOG_ERROR("SecureLayerManager", "Failed to seed PRNG: " + String(ret));
return false;
}
// Setup ECDH with P-256 curve
ret = mbedtls_ecp_group_load(&ecdh_context.grp, MBEDTLS_ECP_DP_SECP256R1);
if (ret != 0) {
LOG_ERROR("SecureLayerManager", "Failed to load ECP group: " + String(ret));
return false;
}
// Generate server key pair
ret = mbedtls_ecdh_gen_public(&ecdh_context.grp, &ecdh_context.d, &ecdh_context.Q,
mbedtls_ctr_drbg_random, &ctr_drbg);
if (ret != 0) {
LOG_ERROR("SecureLayerManager", "Failed to generate server keypair: " + String(ret));
return false;
}
initialized = true;
LOG_INFO("SecureLayerManager", "Secure layer initialized successfully");
return true;
}
void SecureLayerManager::end() {
if (!initialized) return;
// Clear all sessions
sessions.clear();
// Free mbedTLS contexts
mbedtls_ecdh_free(&ecdh_context);
mbedtls_ctr_drbg_free(&ctr_drbg);
mbedtls_entropy_free(&entropy);
initialized = false;
LOG_INFO("SecureLayerManager", "Secure layer shutdown complete");
}
// REMOVED: update() and cleanup code completely removed
// Sessions are cleared automatically when web server stops (10 min timeout)
// No need for manual cleanup with race conditions
String SecureLayerManager::getServerPublicKey() {
if (!initialized) return "";
uint8_t pubkey[65]; // Uncompressed P-256 public key
size_t pubkeyLen;
int ret = mbedtls_ecp_point_write_binary(&ecdh_context.grp, &ecdh_context.Q,
MBEDTLS_ECP_PF_UNCOMPRESSED,
&pubkeyLen, pubkey, sizeof(pubkey));
if (ret != 0) {
LOG_ERROR("SecureLayerManager", "Failed to export public key: " + String(ret));
return "";
}
return bytesToHex(pubkey, pubkeyLen);
}
bool SecureLayerManager::processKeyExchange(const String& clientId, const String& clientPubKeyHex, String& response) {
LOG_INFO("", "KeyExchange START: " + clientId.substring(0,8) + "...");
if (!initialized) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Server not initialized\"}";
return false;
}
SecureSession* session = findSession(clientId);
if (!session) {
session = createSession(clientId);
if (!session) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Session limit exceeded\"}";
return false;
}
}
// Convert hex to binary
uint8_t clientPubKey[65];
if (!hexToBytes(clientPubKeyHex, clientPubKey, sizeof(clientPubKey))) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Invalid public key format\"}";
return false;
}
// Perform ECDH
uint8_t sharedSecret[32];
if (!performECDH(clientPubKey, 65, sharedSecret)) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"ECDH failed\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
return false;
}
// Derive session key using HKDF with client nonce as salt
if (!deriveSessionKey(sharedSecret, session->clientNonce, session->sessionKey)) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Key derivation failed\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
return false;
}
session->keyExchanged = true;
session->lastActivity = millis();
session->rxCounter = 0;
session->txCounter = 0;
// Prepare response with server public key AND salt (for HKDF)
String serverPubKey = getServerPublicKey();
String saltHex = bytesToHex(session->clientNonce, 16);
response = "{\"type\":\"keyexchange\",\"status\":\"success\",\"pubkey\":\"" + serverPubKey + "\",\"salt\":\"" + saltHex + "\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
LOG_INFO("", "KeyExchange OK: " + clientId.substring(0,8) + "... [Sessions:" + String(sessions.size()) + "]");
return true;
}
bool SecureLayerManager::processProtectedKeyExchange(const String& clientId, const String& encryptedClientKey, String& response) {
LOG_INFO("", "Protected KeyExchange START: " + clientId.substring(0,8) + "...");
if (!initialized) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Server not initialized\"}";
return false;
}
// 1. device static key
DeviceStaticKey& deviceKey = DeviceStaticKey::getInstance();
String staticKey = deviceKey.getDeviceStaticKey();
if (staticKey.isEmpty()) {
LOG_ERROR("", "Failed to get device static key");
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Device key unavailable\"}";
return false;
}
// 2. client public key device static key
CryptoManager& crypto = CryptoManager::getInstance();
String clientPubKeyHex = crypto.decryptWithPassword(encryptedClientKey, staticKey);
if (clientPubKeyHex.isEmpty()) {
LOG_ERROR("", "Failed to decrypt client public key with device key");
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Key decryption failed\"}";
return false;
}
LOG_DEBUG("", "Client public key decrypted successfully");
// 3.
SecureSession* session = findSession(clientId);
if (!session) {
session = createSession(clientId);
if (!session) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Session limit exceeded\"}";
return false;
}
}
// 4. hex binary ECDH
uint8_t clientPubKey[65];
if (!hexToBytes(clientPubKeyHex, clientPubKey, sizeof(clientPubKey))) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Invalid decrypted key format\"}";
return false;
}
// 5. ECDH
uint8_t sharedSecret[32];
if (!performECDH(clientPubKey, 65, sharedSecret)) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"ECDH computation failed\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
return false;
}
// 6. Derive session key shared secret
if (!deriveSessionKey(sharedSecret, session->clientNonce, session->sessionKey)) {
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Session key derivation failed\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
return false;
}
// 7.
session->keyExchanged = true;
session->lastActivity = millis();
session->rxCounter = 0;
session->txCounter = 0;
// 8. server public key
String serverPubKey = getServerPublicKey();
String encryptedServerKey = crypto.encryptWithPassword(serverPubKey, staticKey);
if (encryptedServerKey.isEmpty()) {
LOG_ERROR("", "Failed to encrypt server public key");
response = "{\"type\":\"keyexchange\",\"status\":\"error\",\"message\":\"Server key encryption failed\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
return false;
}
// 9. response
response = "{\"type\":\"keyexchange\",\"status\":\"success\",\"encrypted_pubkey\":\"" + encryptedServerKey + "\"}";
secure_memzero(sharedSecret, sizeof(sharedSecret));
LOG_INFO("", "Protected KeyExchange SUCCESS: " + clientId.substring(0,8) + "... [Sessions:" + String(sessions.size()) + "]");
return true;
}
// IRAM_ATTR - IRAM (4-8x boost)
IRAM_ATTR bool SecureLayerManager::encryptResponse(const String& clientId, const String& plaintext, String& encryptedJson) {
if (!initialized) {
LOG_ERROR("SecureLayerManager", "Manager not initialized");
return false;
}
// ANTI-TIMING ANALYSIS: 50-200ms crypto
unsigned long randomDelay = 50 + (esp_random() % 150);
delay(randomDelay);
SecureSession* session = findSession(clientId);
if (!session || !session->keyExchanged) {
// DEBUG -
// TIMING PROTECTION: /
delay(100 + (esp_random() % 100));
return false;
}
// DEBUG -
session->lastActivity = millis();
// Prepare encryption
size_t plaintextLen = plaintext.length();
uint8_t* plaintextBytes = (uint8_t*)plaintext.c_str();
uint8_t* ciphertext = new uint8_t[plaintextLen];
uint8_t iv[SECURE_GCM_IV_SIZE];
uint8_t tag[SECURE_GCM_TAG_SIZE];
size_t ciphertextLen;
// AES-256-GCM encryption via mbedTLS
generateNonce(iv, SECURE_GCM_IV_SIZE);
bool success = encryptData(session->sessionKey, plaintextBytes, plaintextLen,
ciphertext, &ciphertextLen, iv, tag);
if (!success) {
LOG_ERROR("SecureLayerManager", "AES-GCM encryption failed");
}
if (success) {
// Build JSON response
JsonDocument doc;
doc["type"] = "secure";
doc["counter"] = session->txCounter++;
doc["data"] = CryptoManager::getInstance().base64Encode(ciphertext, ciphertextLen);
doc["iv"] = CryptoManager::getInstance().base64Encode(iv, SECURE_GCM_IV_SIZE);
doc["tag"] = CryptoManager::getInstance().base64Encode(tag, SECURE_GCM_TAG_SIZE);
serializeJson(doc, encryptedJson);
// DEBUG -
}
delete[] ciphertext;
return success;
}
// IRAM_ATTR - IRAM
IRAM_ATTR bool SecureLayerManager::decryptRequest(const String& clientId, const String& encryptedJson, String& plaintext) {
if (!initialized) {
LOG_ERROR("SecureLayerManager", "Manager not initialized");
return false;
}
SecureSession* session = findSession(clientId);
if (!session || !session->keyExchanged) {
// DEBUG -
return false;
}
// DEBUG -
// (POST FormData)
if (ESP.getFreeHeap() < 20000) {
LOG_ERROR("SecureLayerManager", "Insufficient heap for JSON parse: " + String(ESP.getFreeHeap()) + "b");
return false;
}
DynamicJsonDocument doc(1024); // 1KB {type, data, iv, tag, counter}
DeserializationError error = deserializeJson(doc, encryptedJson);
if (error) {
LOG_ERROR("SecureLayerManager", "Failed to parse encrypted JSON: " + String(error.c_str()));
LOG_ERROR("SecureLayerManager", "JSON preview: " + encryptedJson.substring(0, 100));
return false;
}
// Extract encrypted data
String dataHex = doc["data"];
String ivHex = doc["iv"];
String tagHex = doc["tag"];
uint64_t counter = doc["counter"];
if (dataHex.isEmpty() || ivHex.isEmpty() || tagHex.isEmpty()) {
LOG_ERROR("SecureLayerManager", "Missing encryption components");
return false;
}
// Check replay protection
if (counter rxCounter) {
LOG_WARNING("SecureLayerManager", "Replay attack detected! Counter: " + String(counter));
return false;
}
session->rxCounter = counter;
// Convert base64 to binary
std::vector dataVec = CryptoManager::getInstance().base64Decode(dataHex);
std::vector ivVec = CryptoManager::getInstance().base64Decode(ivHex);
std::vector tagVec = CryptoManager::getInstance().base64Decode(tagHex);
// Validate decoded sizes
if (dataVec.empty() || ivVec.size() != SECURE_GCM_IV_SIZE || tagVec.size() != SECURE_GCM_TAG_SIZE) {
return false;
}
size_t dataLen = dataVec.size();
uint8_t* ciphertext = new uint8_t[dataLen];
uint8_t iv[SECURE_GCM_IV_SIZE];
uint8_t tag[SECURE_GCM_TAG_SIZE];
// Copy from vectors to buffers
memcpy(ciphertext, dataVec.data(), dataLen);
memcpy(iv, ivVec.data(), SECURE_GCM_IV_SIZE);
memcpy(tag, tagVec.data(), SECURE_GCM_TAG_SIZE);
// AES-256-GCM decryption with tag authentication via mbedTLS
uint8_t* decryptedBytes = new uint8_t[dataLen + 1];
size_t decryptedLen = 0;
bool success = decryptData(session->sessionKey, ciphertext, dataLen,
iv, tag, decryptedBytes, &decryptedLen);
if (success) {
decryptedBytes[decryptedLen] = '\0';
} else {
LOG_ERROR("SecureLayerManager", "AES-GCM auth failed - tag mismatch or tampering");
decryptedBytes[0] = '\0';
}
plaintext = String((char*)decryptedBytes);
session->lastActivity = millis();
// DEBUG -
delete[] ciphertext;
delete[] decryptedBytes;
return success;
}
bool SecureLayerManager::performECDH(const uint8_t* clientPubKey, size_t keyLen, uint8_t* sharedSecret) {
if (!initialized || keyLen != 65) {
LOG_ERROR("", "ECDH check failed");
return false;
}
mbedtls_ecp_point clientPoint, resultPoint;
mbedtls_ecp_point_init(&clientPoint);
mbedtls_ecp_point_init(&resultPoint);
// Import client public key to point
int ret = mbedtls_ecp_point_read_binary(&ecdh_context.grp, &clientPoint,
clientPubKey, keyLen);
if (ret != 0) {
LOG_ERROR("", "ECDH key import failed: " + String(ret));
mbedtls_ecp_point_free(&clientPoint);
mbedtls_ecp_point_free(&resultPoint);
return false;
}
// Compute shared secret: resultPoint = d * clientPoint
ret = mbedtls_ecp_mul(&ecdh_context.grp, &resultPoint, &ecdh_context.d, &clientPoint,
mbedtls_ctr_drbg_random, &ctr_drbg);
if (ret != 0) {
LOG_ERROR("", "ECDH mul failed: " + String(ret));
mbedtls_ecp_point_free(&clientPoint);
mbedtls_ecp_point_free(&resultPoint);
return false;
}
// Extract X coordinate as shared secret
ret = mbedtls_mpi_write_binary(&resultPoint.X, sharedSecret, 32);
mbedtls_ecp_point_free(&clientPoint);
mbedtls_ecp_point_free(&resultPoint);
return ret == 0;
}
bool SecureLayerManager::deriveSessionKey(const uint8_t* sharedSecret, const uint8_t* salt, uint8_t* sessionKey) {
// Custom HKDF implementation using HMAC-SHA256 (ESP32 compatible)
mbedtls_md_context_t ctx;
const mbedtls_md_info_t* md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
if (!md) return false;
mbedtls_md_init(&ctx);
// HKDF-Extract: PRK = HMAC-SHA256(salt, shared_secret)
uint8_t prk[32];
int ret = mbedtls_md_setup(&ctx, md, 1);
if (ret != 0) {
mbedtls_md_free(&ctx);
return false;
}
ret = mbedtls_md_hmac_starts(&ctx, salt, 16);
if (ret == 0) ret = mbedtls_md_hmac_update(&ctx, sharedSecret, 32);
if (ret == 0) ret = mbedtls_md_hmac_finish(&ctx, prk);
if (ret != 0) {
mbedtls_md_free(&ctx);
return false;
}
// HKDF-Expand: derive session key from PRK
const char* info = "SecureLayerV1";
size_t info_len = 13;
ret = mbedtls_md_hmac_starts(&ctx, prk, 32);
if (ret == 0) ret = mbedtls_md_hmac_update(&ctx, (const uint8_t*)info, info_len);
if (ret == 0) {
uint8_t counter = 1;
ret = mbedtls_md_hmac_update(&ctx, &counter, 1);
}
if (ret == 0) ret = mbedtls_md_hmac_finish(&ctx, sessionKey);
mbedtls_md_free(&ctx);
// Clear PRK from memory
memset(prk, 0, sizeof(prk));
return ret == 0;
}
bool SecureLayerManager::encryptData(const uint8_t* key, const uint8_t* plaintext, size_t plaintextLen,
uint8_t* ciphertext, size_t* ciphertextLen,
uint8_t* iv, uint8_t* tag) {
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, SECURE_AES_KEY_SIZE * 8);
if (ret != 0) {
mbedtls_gcm_free(&gcm);
return false;
}
// Generate random IV
generateNonce(iv, SECURE_GCM_IV_SIZE);
ret = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT,
plaintextLen, iv, SECURE_GCM_IV_SIZE,
nullptr, 0, // No additional data
plaintext, ciphertext,
SECURE_GCM_TAG_SIZE, tag);
*ciphertextLen = plaintextLen;
mbedtls_gcm_free(&gcm);
return ret == 0;
}
bool SecureLayerManager::decryptData(const uint8_t* key, const uint8_t* ciphertext, size_t ciphertextLen,
const uint8_t* iv, const uint8_t* tag,
uint8_t* plaintext, size_t* plaintextLen) {
mbedtls_gcm_context gcm;
mbedtls_gcm_init(&gcm);
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, SECURE_AES_KEY_SIZE * 8);
if (ret != 0) {
mbedtls_gcm_free(&gcm);
return false;
}
ret = mbedtls_gcm_auth_decrypt(&gcm, ciphertextLen,
iv, SECURE_GCM_IV_SIZE,
nullptr, 0, // No additional data
tag, SECURE_GCM_TAG_SIZE,
ciphertext, plaintext);
*plaintextLen = ciphertextLen;
mbedtls_gcm_free(&gcm);
return ret == 0;
}
SecureLayerManager::SecureSession* SecureLayerManager::findSession(const String& clientId) {
auto it = sessions.find(clientId);
return (it != sessions.end()) ? &it->second : nullptr;
}
SecureLayerManager::SecureSession* SecureLayerManager::createSession(const String& clientId) {
if (sessions.size() >= SECURE_MAX_SESSIONS) {
// Evict the least recently used session instead of rejecting
auto oldest = sessions.begin();
for (auto it = sessions.begin(); it != sessions.end(); ++it) {
if (it->second.lastActivity < oldest->second.lastActivity) {
oldest = it;
}
}
LOG_INFO("", "Session limit reached evicting LRU: " +
oldest->second.clientId.substring(0,8) + "...");
sessions.erase(oldest);
}
SecureSession session;
session.clientId = clientId;
session.rxCounter = 0;
session.txCounter = 0;
session.keyExchanged = false;
session.lastActivity = millis();
// Generate deterministic client nonce from clientId for reproducible keys
generateNonce(session.clientNonce, 16);
sessions[clientId] = session;
LOG_DEBUG("", "Session created: " + clientId.substring(0,8) + "... [Total:" + String(sessions.size()) + "]");
return &sessions[clientId];
}
void SecureLayerManager::removeSession(const String& clientId) {
auto it = sessions.find(clientId);
if (it != sessions.end()) {
// Clear sensitive data
memset(it->second.sessionKey, 0, SECURE_AES_KEY_SIZE);
sessions.erase(it);
LOG_DEBUG("", "Session removed: " + clientId.substring(0,8) + "... [Remaining:" + String(sessions.size()) + "]");
}
}
// REMOVED: cleanupExpiredSessions() completely deleted
// Reason: Causes race condition crashes without mutex
// Not needed: Web server stops after 10min timeout, clearing all RAM sessions automatically
bool SecureLayerManager::isSecureSessionValid(const String& clientId) {
SecureSession* session = findSession(clientId);
return session && session->keyExchanged;
}
void SecureLayerManager::invalidateSecureSession(const String& clientId) {
removeSession(clientId);
}
int SecureLayerManager::getActiveSecureSessionCount() {
return sessions.size();
}
bool SecureLayerManager::shouldBypassSecurity(const String& endpoint) {
// Static local array to avoid member variable corruption issues
static const char* safeBypassEndpoints[] = {
"/login",
"/register",
"/api/secure/hello",
"/api/secure/keyexchange",
"/api/secure/status",
nullptr
};
for (int i = 0; safeBypassEndpoints[i] != nullptr; i++) {
if (endpoint.startsWith(safeBypassEndpoints[i])) {
return true;
}
}
return false;
}
String SecureLayerManager::wrapSecureResponse(const String& clientId, const String& originalResponse) {
if (!isSecureSessionValid(clientId)) {
return originalResponse; // Return unencrypted if no secure session
}
String encryptedResponse;
if (encryptResponse(clientId, originalResponse, encryptedResponse)) {
return encryptedResponse;
}
return originalResponse; // Fallback to unencrypted
}
bool SecureLayerManager::unwrapSecureRequest(const String& clientId, const String& requestBody, String& unwrappedBody) {
if (!isSecureSessionValid(clientId)) {
unwrappedBody = requestBody;
return true; // Pass through unencrypted
}
return decryptRequest(clientId, requestBody, unwrappedBody);
}
String SecureLayerManager::bytesToHex(const uint8_t* bytes, size_t length) {
String hex = "";
for (size_t i = 0; i < length; i++) {
char hexByte[3];
sprintf(hexByte, "%02x", bytes[i]);
hex += hexByte;
}
return hex;
}
bool SecureLayerManager::hexToBytes(const String& hex, uint8_t* bytes, size_t maxLength) {
if (hex.length() % 2 != 0 || hex.length() / 2 > maxLength) {
return false;
}
size_t length = hex.length() / 2;
for (size_t i = 0; i < length; i++) {
String hexByte = hex.substring(i * 2, i * 2 + 2);
bytes[i] = (uint8_t)strtol(hexByte.c_str(), nullptr, 16);
}
return true;
}
bool SecureLayerManager::generateNonce(uint8_t* nonce, size_t length) {
return mbedtls_ctr_drbg_random(&ctr_drbg, nonce, length) == 0;
}
void SecureLayerManager::wipeAllSessions() {
for (auto& pair : sessions) {
memset(pair.second.sessionKey, 0, SECURE_AES_KEY_SIZE);
memset(pair.second.clientNonce, 0, 16);
}
sessions.clear();
mbedtls_ecdh_free(&ecdh_context);
initialized = false;
}