Security
Security Implementation
Section titled “Security Implementation”Comprehensive security measures and best practices for BCGMCU device-side API implementation.
Transport Layer Security
Section titled “Transport Layer Security”TLS Configuration
Section titled “TLS Configuration”All communication must use HTTPS with TLS 1.2 or higher:
#include <WiFiClientSecure.h>
class SecureHTTPClient {private: WiFiClientSecure client;
public: void initialize() { // Set minimum TLS version client.setMinVersion(esp_tls_proto_version_t::ESP_TLS_VER_TLS_1_2);
// Enable certificate validation client.setInsecure(false); // Never use in production
// Load root CA certificate loadRootCACertificate();
// Optional: Enable certificate pinning enableCertificatePinning(); }
private: void loadRootCACertificate() { // Load from SPIFFS or embed in firmware File certFile = SPIFFS.open("/certs/root-ca.pem", "r"); if (certFile) { String cert = certFile.readString(); client.setCACert(cert.c_str()); certFile.close(); } }
void enableCertificatePinning() { // Pin to specific server certificate fingerprint const char* serverFingerprint = "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD"; client.setFingerprint(serverFingerprint); }};Certificate Management
Section titled “Certificate Management”class CertificateManager {private: struct Certificate { String name; String pem; unsigned long expiry; bool valid; };
std::vector<Certificate> certificates;
public: void loadCertificates() { // Load root CA certificates loadCertificate("root-ca", "/certs/root-ca.pem"); loadCertificate("intermediate", "/certs/intermediate.pem");
// Validate certificates validateAllCertificates(); }
void checkCertificateExpiry() { time_t now = time(nullptr);
for (auto& cert : certificates) { if (cert.expiry - now < 30 * 24 * 3600) { // 30 days Serial.printf("Certificate %s expires in less than 30 days\n", cert.name.c_str()); requestCertificateUpdate(cert.name); } } }
private: void loadCertificate(const String& name, const String& path) { File certFile = SPIFFS.open(path, "r"); if (certFile) { Certificate cert; cert.name = name; cert.pem = certFile.readString(); cert.expiry = extractExpiryDate(cert.pem); cert.valid = validateCertificate(cert.pem);
certificates.push_back(cert); certFile.close(); } }};Authentication & Authorization
Section titled “Authentication & Authorization”JWT Token Management
Section titled “JWT Token Management”#include <ArduinoJWT.h>#include <Preferences.h>
class JWTManager {private: Preferences prefs; String currentToken; unsigned long tokenExpiry = 0; String deviceSecret;
public: void initialize() { prefs.begin("jwt", true); // Read-only mode
// Load device secret from secure storage deviceSecret = prefs.getString("device_secret", "");
if (deviceSecret.isEmpty()) { generateDeviceSecret(); }
// Load stored token currentToken = prefs.getString("jwt_token", ""); tokenExpiry = prefs.getULong("jwt_expiry", 0); }
String getValidToken() { if (isTokenExpired()) { if (!refreshToken()) { // Token refresh failed - need re-authentication return ""; } }
return currentToken; }
bool validateToken(const String& token) { // Parse JWT without signature verification for basic checks ArduinoJWT jwt;
if (!jwt.parse(token)) { return false; }
// Check expiration time_t exp = jwt.getClaim("exp").toInt(); if (time(nullptr) >= exp) { return false; }
// Check issuer String iss = jwt.getClaim("iss"); if (iss != EXPECTED_ISSUER) { return false; }
// Check device ID String deviceId = jwt.getClaim("device_id"); if (deviceId != getDeviceId()) { return false; }
return true; }
private: bool isTokenExpired() { if (currentToken.isEmpty()) return true;
// Check expiry with 5-minute buffer return (millis() > tokenExpiry - 300000); }
void generateDeviceSecret() { // Generate secure random device secret uint8_t randomBytes[32]; esp_fill_random(randomBytes, 32);
// Convert to hex string String secret; for (int i = 0; i < 32; i++) { if (randomBytes[i] < 16) secret += "0"; secret += String(randomBytes[i], HEX); }
deviceSecret = secret;
// Store in secure preferences prefs.end(); prefs.begin("jwt", false); // Read-write mode prefs.putString("device_secret", deviceSecret); prefs.end(); prefs.begin("jwt", true); // Back to read-only }
void storeToken(const String& token) { currentToken = token;
// Extract expiry from token ArduinoJWT jwt; if (jwt.parse(token)) { tokenExpiry = jwt.getClaim("exp").toInt() * 1000; // Convert to milliseconds }
// Store securely prefs.end(); prefs.begin("jwt", false); prefs.putString("jwt_token", token); prefs.putULong("jwt_expiry", tokenExpiry); prefs.end(); prefs.begin("jwt", true); }};API Key Security
Section titled “API Key Security”class APIKeyManager {private: String apiKey; String apiSecret; unsigned long keyRotationTime = 0;
public: void initialize() { loadAPICredentials();
// Schedule key rotation every 30 days keyRotationTime = millis() + (30 * 24 * 60 * 60 * 1000); }
String generateAuthHeader(const String& method, const String& endpoint, const String& payload) { // Create HMAC signature for request String timestamp = String(time(nullptr)); String nonce = generateNonce();
String signatureData = method + "\n" + endpoint + "\n" + timestamp + "\n" + nonce + "\n" + calculateSHA256(payload);
String signature = calculateHMAC(signatureData, apiSecret);
return "BCGMCU-HMAC-SHA256 " + "key=" + apiKey + "," + "timestamp=" + timestamp + "," + "nonce=" + nonce + "," + "signature=" + signature; }
bool shouldRotateKeys() { return millis() >= keyRotationTime; }
private: String generateNonce() { uint8_t randomBytes[16]; esp_fill_random(randomBytes, 16);
String nonce; for (int i = 0; i < 16; i++) { if (randomBytes[i] < 16) nonce += "0"; nonce += String(randomBytes[i], HEX); }
return nonce; }
String calculateHMAC(const String& data, const String& key) { // Implementation using mbedtls HMAC mbedtls_md_context_t ctx; mbedtls_md_init(&ctx);
const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); mbedtls_md_setup(&ctx, info, 1);
mbedtls_md_hmac_starts(&ctx, (const unsigned char*)key.c_str(), key.length()); mbedtls_md_hmac_update(&ctx, (const unsigned char*)data.c_str(), data.length());
unsigned char hmac[32]; mbedtls_md_hmac_finish(&ctx, hmac); mbedtls_md_free(&ctx);
String result; for (int i = 0; i < 32; i++) { if (hmac[i] < 16) result += "0"; result += String(hmac[i], HEX); }
return result; }};Data Encryption
Section titled “Data Encryption”Payload Encryption
Section titled “Payload Encryption”#include <mbedtls/aes.h>#include <mbedtls/gcm.h>
class PayloadEncryption {private: mbedtls_gcm_context gcm; uint8_t encryptionKey[32]; // AES-256 key bool initialized = false;
public: bool initialize() { mbedtls_gcm_init(&gcm);
// Load encryption key from secure storage if (!loadEncryptionKey()) { return false; }
// Setup AES-GCM int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, encryptionKey, 256);
if (ret != 0) { Serial.printf("AES key setup failed: %d\n", ret); return false; }
initialized = true; return true; }
String encryptPayload(const String& plaintext) { if (!initialized) return "";
// Generate random IV uint8_t iv[16]; esp_fill_random(iv, 16);
// Prepare buffers size_t inputLen = plaintext.length(); uint8_t* output = new uint8_t[inputLen]; uint8_t tag[16];
// Encrypt int ret = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, inputLen, iv, 16, nullptr, 0, // No additional data (const uint8_t*)plaintext.c_str(), output, 16, tag);
if (ret != 0) { delete[] output; return ""; }
// Combine IV + encrypted data + tag String encrypted;
// IV (16 bytes) for (int i = 0; i < 16; i++) { if (iv[i] < 16) encrypted += "0"; encrypted += String(iv[i], HEX); }
// Encrypted data for (size_t i = 0; i < inputLen; i++) { if (output[i] < 16) encrypted += "0"; encrypted += String(output[i], HEX); }
// Authentication tag (16 bytes) for (int i = 0; i < 16; i++) { if (tag[i] < 16) encrypted += "0"; encrypted += String(tag[i], HEX); }
delete[] output; return encrypted; }
String decryptPayload(const String& encrypted) { if (!initialized) return "";
// Extract components if (encrypted.length() < 64) return ""; // Minimum size check
// Extract IV (32 hex chars = 16 bytes) uint8_t iv[16]; for (int i = 0; i < 16; i++) { String byteStr = encrypted.substring(i*2, i*2 + 2); iv[i] = strtol(byteStr.c_str(), nullptr, 16); }
// Extract tag (last 32 hex chars = 16 bytes) uint8_t tag[16]; int tagStart = encrypted.length() - 32; for (int i = 0; i < 16; i++) { String byteStr = encrypted.substring(tagStart + i*2, tagStart + i*2 + 2); tag[i] = strtol(byteStr.c_str(), nullptr, 16); }
// Extract encrypted data (middle section) String encryptedData = encrypted.substring(32, tagStart); size_t dataLen = encryptedData.length() / 2;
uint8_t* encryptedBytes = new uint8_t[dataLen]; uint8_t* decryptedBytes = new uint8_t[dataLen];
for (size_t i = 0; i < dataLen; i++) { String byteStr = encryptedData.substring(i*2, i*2 + 2); encryptedBytes[i] = strtol(byteStr.c_str(), nullptr, 16); }
// Decrypt int ret = mbedtls_gcm_auth_decrypt(&gcm, dataLen, iv, 16, nullptr, 0, // No additional data tag, 16, encryptedBytes, decryptedBytes);
String decrypted; if (ret == 0) { decrypted = String((char*)decryptedBytes, dataLen); }
delete[] encryptedBytes; delete[] decryptedBytes;
return decrypted; }
private: bool loadEncryptionKey() { Preferences prefs; prefs.begin("encryption", true);
size_t keyLen = prefs.getBytesLength("aes_key"); if (keyLen != 32) { // Generate new key if not found esp_fill_random(encryptionKey, 32);
// Store new key prefs.end(); prefs.begin("encryption", false); prefs.putBytes("aes_key", encryptionKey, 32); } else { prefs.getBytes("aes_key", encryptionKey, 32); }
prefs.end(); return true; }};Secure Storage
Section titled “Secure Storage”Sensitive Data Protection
Section titled “Sensitive Data Protection”#include <nvs_flash.h>#include <nvs.h>
class SecureStorage {private: nvs_handle_t nvsHandle;
public: bool initialize() { esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { return false; }
err = nvs_open("secure_storage", NVS_READWRITE, &nvsHandle); return (err == ESP_OK); }
bool storeSecret(const String& key, const String& value) { // Encrypt before storing PayloadEncryption encryption; if (!encryption.initialize()) { return false; }
String encryptedValue = encryption.encryptPayload(value); if (encryptedValue.isEmpty()) { return false; }
esp_err_t err = nvs_set_str(nvsHandle, key.c_str(), encryptedValue.c_str()); if (err != ESP_OK) { return false; }
return (nvs_commit(nvsHandle) == ESP_OK); }
String retrieveSecret(const String& key) { size_t required_size = 0; esp_err_t err = nvs_get_str(nvsHandle, key.c_str(), nullptr, &required_size);
if (err != ESP_OK || required_size == 0) { return ""; }
char* encryptedValue = new char[required_size]; err = nvs_get_str(nvsHandle, key.c_str(), encryptedValue, &required_size);
if (err != ESP_OK) { delete[] encryptedValue; return ""; }
// Decrypt retrieved value PayloadEncryption encryption; if (!encryption.initialize()) { delete[] encryptedValue; return ""; }
String decrypted = encryption.decryptPayload(String(encryptedValue)); delete[] encryptedValue;
return decrypted; }
bool deleteSecret(const String& key) { esp_err_t err = nvs_erase_key(nvsHandle, key.c_str()); return (err == ESP_OK && nvs_commit(nvsHandle) == ESP_OK); }
void close() { nvs_close(nvsHandle); }};Input Validation & Sanitization
Section titled “Input Validation & Sanitization”Payload Validation
Section titled “Payload Validation”class SecurityValidator {public: struct ValidationResult { bool valid; std::vector<String> violations; String sanitizedPayload; };
ValidationResult validatePayload(const String& payload) { ValidationResult result; result.valid = true; result.sanitizedPayload = payload;
// Check payload size if (payload.length() > MAX_PAYLOAD_SIZE) { result.valid = false; result.violations.push_back("Payload exceeds maximum size"); return result; }
// Parse JSON StaticJsonDocument<2048> doc; DeserializationError error = deserializeJson(doc, payload);
if (error) { result.valid = false; result.violations.push_back("Invalid JSON format"); return result; }
// Validate required fields if (!doc.containsKey("device_id")) { result.valid = false; result.violations.push_back("Missing device_id"); } else { String deviceId = doc["device_id"]; if (!validateDeviceId(deviceId)) { result.valid = false; result.violations.push_back("Invalid device_id format"); } }
// Validate timestamp if (!doc.containsKey("timestamp")) { result.valid = false; result.violations.push_back("Missing timestamp"); } else { String timestamp = doc["timestamp"]; if (!validateTimestamp(timestamp)) { result.valid = false; result.violations.push_back("Invalid timestamp format");
// Sanitize: replace with current timestamp doc["timestamp"] = getCurrentISO8601(); serializeJson(doc, result.sanitizedPayload); } }
// Validate data ranges validateDataRanges(doc, result);
// Sanitize string fields sanitizeStringFields(doc, result);
return result; }
private: bool validateDeviceId(const String& deviceId) { // Must be alphanumeric with underscores, 3-32 characters if (deviceId.length() < 3 || deviceId.length() > 32) { return false; }
for (char c : deviceId) { if (!isalnum(c) && c != '_') { return false; } }
return true; }
bool validateTimestamp(const String& timestamp) { // Basic ISO8601 format check if (timestamp.length() != 24) return false; if (timestamp.charAt(4) != '-') return false; if (timestamp.charAt(7) != '-') return false; if (timestamp.charAt(10) != 'T') return false; if (timestamp.charAt(19) != '.') return false; if (timestamp.charAt(23) != 'Z') return false;
return true; }
void validateDataRanges(JsonObject& doc, ValidationResult& result) { if (doc.containsKey("bcg_data")) { JsonObject bcg = doc["bcg_data"];
if (bcg.containsKey("pulse_rate")) { int pulse = bcg["pulse_rate"]; if (pulse < 0 || pulse > 300) { result.violations.push_back("Pulse rate out of safe range"); bcg["pulse_rate"] = constrain(pulse, 30, 200); } }
if (bcg.containsKey("respiratory_rate")) { int breathing = bcg["respiratory_rate"]; if (breathing < 0 || breathing > 100) { result.violations.push_back("Respiratory rate out of safe range"); bcg["respiratory_rate"] = constrain(breathing, 5, 40); } } } }
void sanitizeStringFields(JsonObject& doc, ValidationResult& result) { // Remove potential injection characters const char* dangerousChars = "<>\"'&;";
for (JsonPair pair : doc) { if (pair.value().is<const char*>()) { String value = pair.value().as<String>(); String original = value;
// Remove dangerous characters for (const char* c = dangerousChars; *c; c++) { value.replace(*c, ' '); }
// Limit string length if (value.length() > 256) { value = value.substring(0, 256); }
if (value != original) { doc[pair.key()] = value; result.violations.push_back("Sanitized field: " + String(pair.key().c_str())); } } } }
const size_t MAX_PAYLOAD_SIZE = 1024 * 1024; // 1MB};Rate Limiting & DDoS Protection
Section titled “Rate Limiting & DDoS Protection”Request Rate Limiting
Section titled “Request Rate Limiting”class RateLimiter {private: struct RateLimit { unsigned long windowStart; int requestCount; int maxRequests; unsigned long windowSize; };
std::map<String, RateLimit> limits;
public: void initializeLimits() { // Different limits for different endpoint types setLimit("status", 60, 300000); // 60 requests per 5 minutes setLimit("stream", 3600, 3600000); // 3600 requests per hour (1Hz) setLimit("batch", 24, 86400000); // 24 requests per day setLimit("config", 10, 300000); // 10 requests per 5 minutes setLimit("commands", 5, 300000); // 5 requests per 5 minutes }
bool checkRateLimit(const String& endpoint) { if (!limits.count(endpoint)) { return true; // No limit defined }
RateLimit& limit = limits[endpoint]; unsigned long now = millis();
// Reset window if expired if (now - limit.windowStart >= limit.windowSize) { limit.windowStart = now; limit.requestCount = 0; }
// Check if limit exceeded if (limit.requestCount >= limit.maxRequests) { Serial.printf("Rate limit exceeded for %s: %d/%d\n", endpoint.c_str(), limit.requestCount, limit.maxRequests); return false; }
// Increment count limit.requestCount++; return true; }
int getRemainingRequests(const String& endpoint) { if (!limits.count(endpoint)) { return -1; // Unlimited }
RateLimit& limit = limits[endpoint]; return max(0, limit.maxRequests - limit.requestCount); }
private: void setLimit(const String& endpoint, int maxRequests, unsigned long windowSize) { RateLimit limit; limit.windowStart = millis(); limit.requestCount = 0; limit.maxRequests = maxRequests; limit.windowSize = windowSize;
limits[endpoint] = limit; }};Security Monitoring
Section titled “Security Monitoring”Security Event Logging
Section titled “Security Event Logging”class SecurityLogger {private: File logFile;
public: enum EventType { AUTH_FAILURE, RATE_LIMIT_EXCEEDED, INVALID_PAYLOAD, CERTIFICATE_ERROR, ENCRYPTION_FAILURE, SUSPICIOUS_ACTIVITY };
void initialize() { // Open security log file logFile = SPIFFS.open("/logs/security.log", "a"); }
void logSecurityEvent(EventType type, const String& details) { if (!logFile) return;
String timestamp = getCurrentISO8601(); String eventName = getEventTypeName(type);
String logEntry = "[" + timestamp + "] " + eventName + ": " + details + "\n";
logFile.print(logEntry); logFile.flush();
// Also log to serial for immediate visibility Serial.print("SECURITY: " + logEntry);
// Check if we need to report to server if (isCriticalEvent(type)) { reportSecurityEvent(type, details); } }
private: String getEventTypeName(EventType type) { switch (type) { case AUTH_FAILURE: return "AUTH_FAILURE"; case RATE_LIMIT_EXCEEDED: return "RATE_LIMIT_EXCEEDED"; case INVALID_PAYLOAD: return "INVALID_PAYLOAD"; case CERTIFICATE_ERROR: return "CERTIFICATE_ERROR"; case ENCRYPTION_FAILURE: return "ENCRYPTION_FAILURE"; case SUSPICIOUS_ACTIVITY: return "SUSPICIOUS_ACTIVITY"; default: return "UNKNOWN"; } }
bool isCriticalEvent(EventType type) { return (type == AUTH_FAILURE || type == CERTIFICATE_ERROR || type == SUSPICIOUS_ACTIVITY); }
void reportSecurityEvent(EventType type, const String& details) { // Report critical security events to server immediately StaticJsonDocument<256> doc; doc["event_type"] = getEventTypeName(type); doc["details"] = details; doc["device_id"] = getDeviceId(); doc["timestamp"] = getCurrentISO8601();
String payload; serializeJson(doc, payload);
// Send via high-priority channel sendSecurityAlert(payload); }};Security Best Practices Checklist
Section titled “Security Best Practices Checklist”Implementation Guidelines
Section titled “Implementation Guidelines”-
Transport Security
- ✅ Use HTTPS with TLS 1.2+
- ✅ Implement certificate pinning
- ✅ Validate server certificates
- ✅ Use secure cipher suites
-
Authentication
- ✅ Implement JWT token management
- ✅ Automatic token refresh
- ✅ Secure token storage
- ✅ Strong device identity
-
Data Protection
- ✅ Encrypt sensitive payloads
- ✅ Use authenticated encryption (AES-GCM)
- ✅ Implement data integrity checks
- ✅ Secure key management
-
Input Validation
- ✅ Validate all input data
- ✅ Sanitize string inputs
- ✅ Check data ranges
- ✅ Prevent injection attacks
-
Rate Limiting
- ✅ Implement per-endpoint limits
- ✅ Track request patterns
- ✅ Prevent abuse
- ✅ Graceful degradation
-
Monitoring & Logging
- ✅ Log security events
- ✅ Monitor for anomalies
- ✅ Report critical issues
- ✅ Regular security audits
-
Secure Storage
- ✅ Encrypt stored secrets
- ✅ Use secure preferences
- ✅ Protect encryption keys
- ✅ Implement key rotation
-
Error Handling
- ✅ Don’t leak sensitive info
- ✅ Fail securely
- ✅ Log security failures
- ✅ Implement recovery procedures