[BLE] Add support for decrypting PVVX, BTHome v2 and Victron BLE frames (#2219)

* Changing WebUI to include display device name, and change it to select drop down rather than checkbox

* Fix mqttDiscovery to require WebUI and ESP32 for displayDeviceName

* Fix mqttDiscovery to require WebUI and ESP32 and ESP8266 for displayDeviceName and ForceDeviceName

* Changing WebUI to include display device name, and change it to select drop down rather than checkbox

* Fixes for WebUI and BT for supporting custom setting Display name

* Fixes for WebUI and BT for supporting custom setting Display name

* Move DISPLAY_DEVICE_NAME to User_config.h

* Update docs to include change for Display temperature

* Update docs to include change for Display temperature

* Fix minor cosmetic bug where devices were not linking in HA to the gateway using via_device as it should be the gateway mac address not name

* Add support for decrypting BTHome v2 frames

* Add support for decrypting BTHome v2 frames

* Add support for decrypting BTHome v2 frames

* BTHome fix issue with theengs-plug

* BTHome fix issue with theengs-plug

* Adding support for all BLE encrypted methods, support in UI and gatewayBT for specific MACAddress AES Keys

* Fix lint

* Fix build issue with theengs-bridge-v11 and esp32dev-all-test and revert the documentation to Units of measurement displayed

* Revert docs

* Revert displayDeviceName and Units of measurement

* Revert displayDeviceName and Units of measurement

* Revert displayDeviceName and Units of measurement

* Revert minor typo

* Revert minor typo

* Revert minor typo

* Bug in Victron as nonce should be 16 bytes

* Shortened the client side javascript for BLE key validation that is commented out due to image constrains on theengs-bridge-v11
This commit is contained in:
Peter Lambrechtsen authored and GitHub committed 2025-08-06 19:59:33 -05:00
1 parent a0d5b7d181
commit 2d29ee4524
7 files changed
+399 -1

No files matched your search

+232
View File
@@ -57,6 +57,10 @@ BTConfig_s BTConfig;
# if BLEDecoder
# include <decoder.h>
# if BLEDecryptor
# include "mbedtls/ccm.h"
# include "mbedtls/aes.h"
# endif
TheengsDecoder decoder;
# endif
@@ -1154,6 +1158,27 @@ void launchBTDiscovery(bool overrideDiscovery) {
void launchBTDiscovery(bool overrideDiscovery) {}
# endif
# if BLEDecryptor
// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it
int hexToBytes(String hex, uint8_t *out, size_t maxLen) {
int len = hex.length();
if (len % 2 || len / 2 > maxLen) return -1;
for (int i = 0, j = 0; i < len; i += 2, j++) {
out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16);
}
return len / 2;
}
// Reverse bytes
void reverseBytes(uint8_t *data, size_t length) {
size_t i;
for (i = 0; i < length / 2; i++) {
uint8_t temp = data[i];
data[i] = data[length - 1 - i];
data[length - 1 - i] = temp;
}
}
# endif
# if BLEDecoder
void process_bledata(JsonObject& BLEdata) {
yield(); // Necessary to let the loop run in case of connectivity issues
@@ -1166,6 +1191,213 @@ void process_bledata(JsonObject& BLEdata) {
int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata);
int mac_type = BLEdata["mac_type"].as<int>();
# if BLEDecryptor
if (BLEdata["encr"] && (BLEdata["encr"].as<int>() >0 && BLEdata["encr"].as<int>() <=2)) {
// Decrypting Encrypted BLE Data PVVX, BTHome or Victron
Log.trace(F("[BLEDecryptor] Decrypt ENCR:%d ModelID:%s Payload:%s" CR), BLEdata["encr"].as<int>(), BLEdata["model_id"].as<const char*>(), BLEdata["cipher"].as<const char*>());
// MAC address
String macWOdots = BLEdata["id"].as<String>(); // Mac Address without dots
macWOdots.replace(":", "");
unsigned char macAddress[6];
int maclen = hexToBytes(macWOdots, macAddress, 6);
if (maclen != 6) {
Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen);
return;
}
// AES decryption key
unsigned char bleaeskey[16];
int bleaeskeylength = 0;
if (ble_aes_keys.containsKey(macWOdots)){
Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as<const char*>());
bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16);
} else {
Log.trace(F("[BLEDecryptor] Default AES key" CR));
bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16);
}
// Check AES Key
if (bleaeskeylength != 16) {
Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength);
return;
}
// Build nonce and aad
uint8_t nonce[16];
int noncelength = 0;
unsigned char aad[1];
int aadLength;
if (BLEdata["encr"].as<int>() == 1){ // PVVX Encrypted
noncelength = 11; // 11 bytes
reverseBytes(macAddress, 6); // 6 bytes: device address in reverse
memcpy(nonce, macAddress, 6);
int maclen = hexToBytes(macWOdots, macAddress, 6);
unsigned char servicedata[16];
int servicedatalen = hexToBytes(BLEdata["servicedata"].as<String>(), servicedata, 16);
nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID)
nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID"
nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian
nonce[9] = 0x18; //
unsigned char ctr[1]; // 1 byte : counter
int ctrlen = hexToBytes(BLEdata["ctr"].as<String>(), ctr, 1);
if (ctrlen != 1) {
Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen);
return;
}
nonce[10] = ctr[0];
aad[0] = 0x11;
aadLength = 1;
Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str());
} else if (BLEdata["encr"].as<int>() == 2){ // BTHome V2 Encrypted
noncelength = 13; // 13 bytes
memcpy(nonce, macAddress, 6);
nonce[6] = 0xD2; // UUID
nonce[7] = 0xFC;
nonce[8] = 0x41; // BTHome Device Data encrypted payload byte
unsigned char ctr[4]; // Counter
int ctrlen = hexToBytes(BLEdata["ctr"].as<String>(), ctr, 4);
if (ctrlen != 4) {
Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen);
return;
}
memcpy(&nonce[9], ctr, 4);
aad[0] = 0x00;
aadLength = 0;
Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str());
} else if (BLEdata["encr"].as<int>() == 3){
nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7
unsigned char iv[2];
int ivlen = hexToBytes(BLEdata["ctr"].as<String>(), iv, 2);
if (ivlen != 2) {
Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen);
return;
}
memcpy(nonce, iv, 2);
Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str());
} else {
return; // No match
}
// Ciphertext to bytes
int cipherlen = sizeof(BLEdata["cipher"].as<String>());
unsigned char ciphertext[cipherlen];
int ciphertextlen = hexToBytes(BLEdata["cipher"].as<String>(), ciphertext, cipherlen);
unsigned char decrypted[ciphertextlen]; // Decrypted payload
// Decrypt ciphertext
if (BLEdata["encr"].as<int>() == 1 || BLEdata["encr"].as<int>() == 2) {
// Decrypt PVVX and BTHome V2 ciphertext using AES CCM
mbedtls_ccm_context ctx;
mbedtls_ccm_init(&ctx);
if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) {
Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR));
return;
}
// Message Integrity Check (MIC)
unsigned char mic[4];
int miclen = hexToBytes(BLEdata["mic"].as<String>(), mic, 4);
if (miclen != 4) {
Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen);
return;
}
int ret = mbedtls_ccm_auth_decrypt(
&ctx, // AES Key
ciphertextlen, // length of ciphertext
nonce, noncelength, // Nonce
aad, aadLength, // AAD
ciphertext, // input ciphertext
decrypted, // output plaintext
mic, sizeof(mic) // Message Integrity Check
);
mbedtls_ccm_free(&ctx);
if (ret == 0) {
Log.notice(F("[BLEDecryptor] Decryption successful" CR));
} else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) {
Log.error(F("[BLEDecryptor] Authentication failed." CR));
return;
} else {
Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret);
return;
}
// Build new servicedata
if (BLEdata["encr"].as<int>() == 1){ // PVVX
BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen);
} else if (BLEdata["encr"].as<int>() == 2) { // BTHomeV2
// Build new servicedata
uint8_t newservicedata[3 + ciphertextlen];
newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type
newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not
newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0?
memcpy(&newservicedata[3], decrypted, ciphertextlen);
BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3);
} else {
return;
}
Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as<const char*>());
} else if (BLEdata["encr"].as<int>() == 3) {
// Decrypt Victron Energy encrypted advertisements.
size_t nc_off = 0;
uint8_t stream_block[16] = {0};
mbedtls_aes_context ctx;
mbedtls_aes_init(&ctx);
mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128);
int ret = mbedtls_aes_crypt_ctr(
&ctx, // AES Key
ciphertextlen, // length of ciphertext
&nc_off,
nonce, // 16 byte nonce with 2 bytes iv
stream_block,
ciphertext, // input ciphertext
decrypted // output plaintext
);
mbedtls_aes_free(&ctx);
if (ret == 0) {
Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR));
} else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) {
Log.error(F("[BLEDecryptor] Victron Authentication failed." CR));
return;
} else {
Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret);
return;
}
// Build new manufacturerdata
unsigned char manufacturerdata[10 + ciphertextlen];
int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as<String>(), manufacturerdata, 10);
manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data
manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data
manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data
memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data
BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata
Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as<const char*>());
}
// Print before and after decoder post decryption
// serializeJsonPretty(BLEdata, Serial);
model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata);
// serializeJsonPretty(BLEdata, Serial);
Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id);
// Remove the cipher fields from BLEdata
BLEdata.remove("encr");
BLEdata.remove("cipher");
BLEdata.remove("ctr");
BLEdata.remove("mic");
}
# endif
// Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding
if (BLEdata["prmac"]) {
BLEdata.remove("prmac");