Skip to content

Security

Comprehensive security measures and best practices for BCGMCU device-side API implementation.

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);
}
};
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();
}
}
};
#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);
}
};
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;
}
};
#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;
}
};
#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);
}
};
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
};
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;
}
};
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);
}
};
  1. Transport Security

    • ✅ Use HTTPS with TLS 1.2+
    • ✅ Implement certificate pinning
    • ✅ Validate server certificates
    • ✅ Use secure cipher suites
  2. Authentication

    • ✅ Implement JWT token management
    • ✅ Automatic token refresh
    • ✅ Secure token storage
    • ✅ Strong device identity
  3. Data Protection

    • ✅ Encrypt sensitive payloads
    • ✅ Use authenticated encryption (AES-GCM)
    • ✅ Implement data integrity checks
    • ✅ Secure key management
  4. Input Validation

    • ✅ Validate all input data
    • ✅ Sanitize string inputs
    • ✅ Check data ranges
    • ✅ Prevent injection attacks
  5. Rate Limiting

    • ✅ Implement per-endpoint limits
    • ✅ Track request patterns
    • ✅ Prevent abuse
    • ✅ Graceful degradation
  6. Monitoring & Logging

    • ✅ Log security events
    • ✅ Monitor for anomalies
    • ✅ Report critical issues
    • ✅ Regular security audits
  7. Secure Storage

    • ✅ Encrypt stored secrets
    • ✅ Use secure preferences
    • ✅ Protect encryption keys
    • ✅ Implement key rotation
  8. Error Handling

    • ✅ Don’t leak sensitive info
    • ✅ Fail securely
    • ✅ Log security failures
    • ✅ Implement recovery procedures