This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025:lab2 [2025/10/07 13:16] dan.tudose [CTS Service on NimBLE] |
iothings:laboratoare:2025:lab2 [2025/10/07 13:44] (current) dan.tudose [BLE Security and Pairing] |
||
|---|---|---|---|
| Line 863: | Line 863: | ||
| ==== BLE Security and Pairing ==== | ==== BLE Security and Pairing ==== | ||
| + | All BLE connections done so far in this lab have been without any authentication/encryption. For applications where this is necessary, BLE offers multiple security modes and a bonding scheme which establishes and encrypted connection between devices. | ||
| + | ^ Mode | Level | Description ^ | ||
| + | | **Mode 1, Level 1** | No security | No encryption or authentication | | ||
| + | | **Mode 1, Level 2** | Unauthenticated pairing with encryption | "Just Works" — no MITM protection | | ||
| + | | **Mode 1, Level 3** | Authenticated pairing with encryption | Uses Passkey/Numeric Comparison | | ||
| + | | **Mode 1, Level 4** | Authenticated LE Secure Connections | Uses modern ECDH key exchange | | ||
| + | |||
| + | |||
| + | A typical pairing scenarion over BLE goes through these steps: | ||
| + | |||
| + | 1. **Pairing Request/Response:** Devices agree on IO capabilities (keyboard, display, none). | ||
| + | 2. **Key Exchange:** Exchange temporary keys (STK, LTK, IRK, etc.). | ||
| + | 3. **Encryption Setup:** Link is encrypted using generated keys. | ||
| + | 4. **Bonding (optional):** Keys are stored persistently for future use. | ||
| + | |||
| + | We can test pairing with the nRF Connect mobile app. | ||
| + | |||
| + | - Flash your ESP32-C6 firmware with the code below, which has security-enabled GATT characteristics. | ||
| + | - Open **nRF Connect → Scan → Connect** to your ESP32-C6. | ||
| + | - Try to **read** a protected characteristic — expect something like: <code>Error 0x05: Insufficient Authentication</code> | ||
| + | - Tap **⋮ → Bond** to start pairing. | ||
| + | - Depending on setup: | ||
| + | * **Just Works:** automatic pairing. | ||
| + | * **Passkey Entry:** app prompts for a 6-digit PIN. | ||
| + | * **Numeric Comparison:** both sides show a code for confirmation. | ||
| + | - After pairing, retry read — it should succeed. | ||
| <code C main.cpp> | <code C main.cpp> | ||
| #include <Arduino.h> | #include <Arduino.h> | ||
| + | #include <Wire.h> | ||
| + | #include <cmath> | ||
| + | #include <Adafruit_BME680.h> | ||
| #include <NimBLEDevice.h> | #include <NimBLEDevice.h> | ||
| #include <NimBLEAdvertisementData.h> | #include <NimBLEAdvertisementData.h> | ||
| + | |||
| + | |||
| namespace { | namespace { | ||
| - | constexpr char kDeviceName[] = "Dan ESP32C6 Secure GATT"; | + | #define I2C_SDA_PIN 21 |
| - | const NimBLEUUID kSecureServiceUUID("12345678-90ab-cdef-1234-567890abcdef"); | + | #define I2C_SCL_PIN 22 |
| - | const NimBLEUUID kSecureValueCharUUID("12345678-90ab-cdef-1234-567890abcdea"); | + | #define BME680_I2C_ADDR 0x77 |
| - | const NimBLEUUID kSecureControlCharUUID("12345678-90ab-cdef-1234-567890abcdeb"); | + | |
| + | constexpr char kDeviceName[] = "ESP32C6 ESS"; | ||
| + | const NimBLEUUID kEnvironmentalServiceUUID((uint16_t)0x181A); | ||
| + | const NimBLEUUID kTemperatureCharUUID((uint16_t)0x2A6E); | ||
| + | const NimBLEUUID kHumidityCharUUID((uint16_t)0x2A6F); | ||
| + | const NimBLEUUID kPressureCharUUID((uint16_t)0x2A6D); | ||
| constexpr uint32_t kSecurityPasskey = 654321; // 6-digit static passkey for pairing | constexpr uint32_t kSecurityPasskey = 654321; // 6-digit static passkey for pairing | ||
| - | constexpr uint32_t kNotifyIntervalMs = 5000; // send secure notify every 5 seconds | + | constexpr uint32_t kNotifyIntervalMs = 5000; // send notify every 5 seconds |
| - | NimBLECharacteristic* g_valueCharacteristic = nullptr; | + | Adafruit_BME680 g_bme; |
| - | NimBLECharacteristic* g_controlCharacteristic = nullptr; | + | bool g_bmeReady = false; |
| + | NimBLECharacteristic* g_temperatureCharacteristic = nullptr; | ||
| + | NimBLECharacteristic* g_humidityCharacteristic = nullptr; | ||
| + | NimBLECharacteristic* g_pressureCharacteristic = nullptr; | ||
| volatile bool g_isConnected = false; | volatile bool g_isConnected = false; | ||
| uint32_t g_lastNotify = 0; | uint32_t g_lastNotify = 0; | ||
| Line 922: | Line 961: | ||
| class ValueCallbacks : public NimBLECharacteristicCallbacks { | class ValueCallbacks : public NimBLECharacteristicCallbacks { | ||
| void onRead(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { | void onRead(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { | ||
| - | Serial.printf("[GATT] Secure read from %s, value='%s'\n", | + | Serial.printf("[GATT] Read from %s on characteristic %s\n", |
| connInfo.getAddress().toString().c_str(), | connInfo.getAddress().toString().c_str(), | ||
| - | characteristic->getValue().c_str()); | + | characteristic->getUUID().toString().c_str()); |
| } | } | ||
| void onWrite(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { | void onWrite(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { | ||
| std::string value = characteristic->getValue(); | std::string value = characteristic->getValue(); | ||
| - | Serial.printf("[GATT] Secure write from %s, value='%s'\n", | + | Serial.printf("[GATT] Write to %s from %s, %zu bytes\n", |
| + | characteristic->getUUID().toString().c_str(), | ||
| connInfo.getAddress().toString().c_str(), | connInfo.getAddress().toString().c_str(), | ||
| - | value.c_str()); | + | value.size()); |
| } | } | ||
| }; | }; | ||
| - | class ControlCallbacks : public NimBLECharacteristicCallbacks { | + | ServerCallbacks g_serverCallbacks; |
| - | void onWrite(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { | + | ValueCallbacks g_valueCallbacks; |
| - | const std::string value = characteristic->getValue(); | + | |
| - | Serial.printf("[CTRL] Command from %s: '%s'\n", connInfo.getAddress().toString().c_str(), value.c_str()); | + | |
| - | if (value == "reset") { | + | bool initBME680() { |
| - | g_valueCharacteristic->setValue("secure-data-reset"); | + | if (!g_bme.begin(BME680_I2C_ADDR)) { |
| - | g_valueCharacteristic->notify(); | + | Serial.println("[BME680] Not found on 0x77, trying 0x76..."); |
| - | Serial.println("[CTRL] Value characteristic reset & notified"); | + | if (!g_bme.begin(0x76)) { |
| + | Serial.println("[BME680] Sensor not found. Check wiring/power."); | ||
| + | return false; | ||
| } | } | ||
| } | } | ||
| - | }; | ||
| - | ServerCallbacks g_serverCallbacks; | + | g_bme.setTemperatureOversampling(BME680_OS_8X); |
| - | ValueCallbacks g_valueCallbacks; | + | g_bme.setHumidityOversampling(BME680_OS_2X); |
| - | ControlCallbacks g_controlCallbacks; | + | g_bme.setPressureOversampling(BME680_OS_4X); |
| + | g_bme.setIIRFilterSize(BME680_FILTER_SIZE_3); | ||
| + | g_bme.setGasHeater(0, 0); // Disable gas heater for periodic environmental sampling | ||
| + | |||
| + | Serial.println("[BME680] Sensor initialized"); | ||
| + | return true; | ||
| + | } | ||
| + | |||
| + | bool readBME680(float& temperatureC, float& humidityPct, float& pressurePa) { | ||
| + | if (!g_bme.performReading()) { | ||
| + | Serial.println("[BME680] Failed to perform reading"); | ||
| + | return false; | ||
| + | } | ||
| + | |||
| + | temperatureC = g_bme.temperature; | ||
| + | humidityPct = g_bme.humidity; | ||
| + | pressurePa = g_bme.pressure; // Library returns Pascals | ||
| + | return true; | ||
| + | } | ||
| void setupSecurity() { | void setupSecurity() { | ||
| Line 963: | Line 1020: | ||
| server->setCallbacks(&g_serverCallbacks, false); | server->setCallbacks(&g_serverCallbacks, false); | ||
| - | NimBLEService* secureService = server->createService(kSecureServiceUUID); | + | NimBLEService* environmentalService = server->createService(kEnvironmentalServiceUUID); |
| - | g_valueCharacteristic = secureService->createCharacteristic( | + | g_temperatureCharacteristic = environmentalService->createCharacteristic( |
| - | kSecureValueCharUUID, | + | kTemperatureCharUUID, |
| NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | | ||
| NIMBLE_PROPERTY::READ_AUTHEN | | NIMBLE_PROPERTY::READ_AUTHEN | | ||
| - | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | | ||
| - | NIMBLE_PROPERTY::WRITE_AUTHEN | | ||
| NIMBLE_PROPERTY::NOTIFY); | NIMBLE_PROPERTY::NOTIFY); | ||
| - | g_valueCharacteristic->setCallbacks(&g_valueCallbacks); | + | g_temperatureCharacteristic->setCallbacks(&g_valueCallbacks); |
| - | g_valueCharacteristic->setValue("secure-data-init"); | + | uint8_t tempPlaceholder[2] = {0xFF, 0x7F}; // NaN equivalent (0x7FFF) |
| + | g_temperatureCharacteristic->setValue(tempPlaceholder, sizeof(tempPlaceholder)); | ||
| - | g_controlCharacteristic = secureService->createCharacteristic( | + | g_humidityCharacteristic = environmentalService->createCharacteristic( |
| - | kSecureControlCharUUID, | + | kHumidityCharUUID, |
| - | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | | + | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | |
| - | NIMBLE_PROPERTY::WRITE_AUTHEN | | + | NIMBLE_PROPERTY::READ_AUTHEN | |
| - | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | | + | NIMBLE_PROPERTY::NOTIFY); |
| - | NIMBLE_PROPERTY::READ_AUTHEN); | + | g_humidityCharacteristic->setCallbacks(&g_valueCallbacks); |
| - | g_controlCharacteristic->setCallbacks(&g_controlCallbacks); | + | uint8_t humidityPlaceholder[2] = {0xFF, 0xFF}; |
| - | g_controlCharacteristic->setValue("pending"); | + | g_humidityCharacteristic->setValue(humidityPlaceholder, sizeof(humidityPlaceholder)); |
| + | |||
| + | g_pressureCharacteristic = environmentalService->createCharacteristic( | ||
| + | kPressureCharUUID, | ||
| + | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | | ||
| + | NIMBLE_PROPERTY::READ_AUTHEN | | ||
| + | NIMBLE_PROPERTY::NOTIFY); | ||
| + | g_pressureCharacteristic->setCallbacks(&g_valueCallbacks); | ||
| + | uint8_t pressurePlaceholder[4] = {0xFF, 0xFF, 0xFF, 0xFF}; | ||
| + | g_pressureCharacteristic->setValue(pressurePlaceholder, sizeof(pressurePlaceholder)); | ||
| - | secureService->start(); | + | environmentalService->start(); |
| NimBLEAdvertising* advertising = NimBLEDevice::getAdvertising(); | NimBLEAdvertising* advertising = NimBLEDevice::getAdvertising(); | ||
| NimBLEAdvertisementData advData; | NimBLEAdvertisementData advData; | ||
| advData.setName(kDeviceName); | advData.setName(kDeviceName); | ||
| - | advData.addServiceUUID(kSecureServiceUUID); | + | advData.addServiceUUID(kEnvironmentalServiceUUID); |
| - | advData.setAppearance(0x0080); // Generic Computer | + | advData.setAppearance(0x0341); // Thermometer |
| advertising->setAdvertisementData(advData); | advertising->setAdvertisementData(advData); | ||
| NimBLEAdvertisementData scanData; | NimBLEAdvertisementData scanData; | ||
| - | scanData.setName("Secure GATT", false); | + | scanData.setName("Env Sensing", false); |
| advertising->setScanResponseData(scanData); | advertising->setScanResponseData(scanData); | ||
| Line 1001: | Line 1066: | ||
| if (advertising->start()) { | if (advertising->start()) { | ||
| - | Serial.println("[BLE] Advertising secure GATT service"); | + | Serial.println("[BLE] Advertising environmental sensing service"); |
| } else { | } else { | ||
| Serial.println("[BLE] Failed to start advertising"); | Serial.println("[BLE] Failed to start advertising"); | ||
| Line 1007: | Line 1072: | ||
| } | } | ||
| - | void notifySecureValue() { | + | void updateEnvironmentalMeasurements() { |
| - | if (!g_isConnected || g_valueCharacteristic == nullptr) { | + | if (!g_isConnected || !g_bmeReady || |
| + | g_temperatureCharacteristic == nullptr || | ||
| + | g_humidityCharacteristic == nullptr || | ||
| + | g_pressureCharacteristic == nullptr) { | ||
| return; | return; | ||
| } | } | ||
| Line 1018: | Line 1086: | ||
| g_lastNotify = now; | g_lastNotify = now; | ||
| - | char buffer[32]; | + | float temperatureC = 0.0f; |
| - | snprintf(buffer, sizeof(buffer), "tick-%lu", static_cast<unsigned long>(now / 1000)); | + | float humidityPct = 0.0f; |
| - | g_valueCharacteristic->setValue(buffer); | + | float pressurePa = 0.0f; |
| - | g_valueCharacteristic->notify(); | + | if (!readBME680(temperatureC, humidityPct, pressurePa)) { |
| - | Serial.printf("[GATT] Notified encrypted value '%s'\n", buffer); | + | return; |
| + | } | ||
| + | |||
| + | const int16_t temperatureHundredths = | ||
| + | static_cast<int16_t>(std::lround(temperatureC * 100.0)); | ||
| + | const uint16_t humidityHundredths = | ||
| + | static_cast<uint16_t>(std::lround(humidityPct * 100.0)); | ||
| + | const uint32_t pressureValue = static_cast<uint32_t>(std::lround(pressurePa)); | ||
| + | |||
| + | uint8_t temperaturePayload[2] = { | ||
| + | static_cast<uint8_t>(temperatureHundredths & 0xFF), | ||
| + | static_cast<uint8_t>((temperatureHundredths >> 8) & 0xFF), | ||
| + | }; | ||
| + | g_temperatureCharacteristic->setValue(temperaturePayload, sizeof(temperaturePayload)); | ||
| + | g_temperatureCharacteristic->notify(); | ||
| + | |||
| + | uint8_t humidityPayload[2] = { | ||
| + | static_cast<uint8_t>(humidityHundredths & 0xFF), | ||
| + | static_cast<uint8_t>((humidityHundredths >> 8) & 0xFF), | ||
| + | }; | ||
| + | g_humidityCharacteristic->setValue(humidityPayload, sizeof(humidityPayload)); | ||
| + | g_humidityCharacteristic->notify(); | ||
| + | |||
| + | uint8_t pressurePayload[4] = { | ||
| + | static_cast<uint8_t>(pressureValue & 0xFF), | ||
| + | static_cast<uint8_t>((pressureValue >> 8) & 0xFF), | ||
| + | static_cast<uint8_t>((pressureValue >> 16) & 0xFF), | ||
| + | static_cast<uint8_t>((pressureValue >> 24) & 0xFF), | ||
| + | }; | ||
| + | g_pressureCharacteristic->setValue(pressurePayload, sizeof(pressurePayload)); | ||
| + | g_pressureCharacteristic->notify(); | ||
| + | |||
| + | Serial.printf("[ESS] Notified T=%.2fC H=%.2f%% P=%.2fhPa\n", | ||
| + | temperatureC, | ||
| + | humidityPct, | ||
| + | pressurePa / 100.0f); | ||
| } | } | ||
| Line 1034: | Line 1137: | ||
| delay(1000); | delay(1000); | ||
| Serial.println(); | Serial.println(); | ||
| - | Serial.println("[BOOT] ESP32-C6 secure GATT server"); | + | Serial.println("[BOOT] ESP32-C6 Environmental Sensing Server"); |
| + | |||
| + | Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); | ||
| + | g_bmeReady = initBME680(); | ||
| + | if (!g_bmeReady) { | ||
| + | Serial.println("[BOOT] BME680 not ready; characteristics will remain invalid"); | ||
| + | } | ||
| NimBLEDevice::init(kDeviceName); | NimBLEDevice::init(kDeviceName); | ||
| Line 1042: | Line 1151: | ||
| void loop() { | void loop() { | ||
| - | notifySecureValue(); | + | updateEnvironmentalMeasurements(); |
| delay(100); | delay(100); | ||
| } | } | ||
| + | |||
| </code> | </code> | ||