Skip to content

Commit 2d29ee4

Browse files
[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
1 parent a0d5b7d commit 2d29ee4

File tree

7 files changed

+399
-1
lines changed

7 files changed

+399
-1
lines changed

docs/use/webui.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ Ability to change the mqtt settings, if the change is unsuccessful it will rever
3030

3131
Ability to change the display of sensor to Metric or Imperial, and disable the WebUI Authentication
3232

33+
## Bluetooth Low Energy - BLE
34+
35+
Ability to add the default AES BLE decryption key, and multiple per-device `macaddress:aeskey` allowing for devices that cannot have their AES key changed.
36+
3337
## Logging
3438

3539
Ability to temporarily change the logging level.

main/TheengsCommon.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ extern char mqtt_topic[];
6363
extern char gateway_name[];
6464
extern unsigned long lastDiscovery; // Time of the last discovery to trigger automaticaly to off after DiscoveryAutoOffTimer
6565

66+
#if BLEDecryptor
67+
extern char ble_aes[];
68+
extern StaticJsonDocument<JSON_BLE_AES_CUSTOM_KEYS> ble_aes_keys;
69+
#endif
70+
6671
extern bool enqueueJsonObject(const StaticJsonDocument<JSON_MSG_BUFFER>& jsonDoc, int timeout);
6772
extern bool enqueueJsonObject(const StaticJsonDocument<JSON_MSG_BUFFER>& jsonDoc);
6873
extern void buildTopicFromId(JsonObject& Jsondata, const char* origin);

main/User_config.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,18 @@ bool isAduplicateSignal(uint64_t);
652652
void storeSignalValue(uint64_t);
653653
#endif
654654

655+
#ifdef ZgatewayBT
656+
# ifndef BLEDecryptor
657+
# define BLEDecryptor true //true if decrypt encrypted PVVX or BTHome v2 service data
658+
# endif
659+
# ifndef JSON_BLE_AES_CUSTOM_KEYS
660+
# define JSON_BLE_AES_CUSTOM_KEYS 256 // 42 byte BLE Custom Key * 6 rounded up to 256.
661+
# endif
662+
# ifndef BLE_AES
663+
# define BLE_AES "00112233445566778899001122334455"
664+
# endif
665+
#endif
666+
655667
#define convertTemp_CtoF(c) ((c * 1.8) + 32)
656668
#define convertTemp_FtoC(f) ((f - 32) * 5 / 9)
657669

main/config_WebContent.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@
6262
#else
6363
# define configure_6
6464
#endif
65-
#define configure_7
65+
#ifdef BLEDecryptor
66+
# define configure_7 "<p><form action='bl' method='post'><button>Configure BLE</button></form></p>"
67+
#else
68+
# define configure_7
69+
#endif
6670
#define configure_8
6771

6872
/*------------------- ----------------------*/
@@ -219,6 +223,15 @@ const char config_lora_body[] = body_header
219223
"</form>"
220224
"</fieldset>" body_footer_config_menu;
221225

226+
#ifdef BLEDecryptor
227+
const char config_ble_body[] = body_header "<fieldset class=\"set1\"><legend><span><b>Configure BLE</b></span></legend><form method='post' action='bl'><p><b>BLE AES Default Key (32 char hex)</b></p><input id='bk' name='bk' minlength='32' maxlength='32' placeholder=" BLE_AES " value='%s' oninput='bkv()'><p id='bke' style='color:red;'></p><hr><p><b>BLE Key Pairs</b></p><p>MacAddress:AESKey with space separator</p><p><textarea id='kp' name='kp' placeholder='A4C138012345:00112233445566778899001122334455' rows='3' cols='46' oninput='kpv()'>%s</textarea></p><p id='kpe' style='color:red;'></p><br><button id='s' name='save' type='submit' class='button bgrn'>Save</button></form></fieldset>" body_footer_config_menu;
228+
229+
// Client side javascript validation of the Default AES Key is hex, and the custom keys are in the correct format. This pushes the theengs-bridge-v11 way of the file size, so reudcing it now and leaving it commented out
230+
//const char ble_script[] = "function bkv(){let e=document.getElementById('bk'),t=document.getElementById('bke'),l=e.value.trim();0===l.length||/^[A-Fa-f0-9]{32}$/.test(l)?(t.textContent='',e.style.color=''):(t.textContent='Invalid key',e.style.color='red')}function kpv(){let e=document.getElementById('kp'),t=document.getElementById('kpe'),l=e.value.split(/ /),n=/^[A-Fa-f0-9]{12}:[A-Fa-f0-9]{32}$/,o=l.map((e,t)=>({line:e,index:t})).filter(e=>!n.test(e.line));if(0===e.value.trim().length||0===o.length)t.textContent='',e.style.color='';else{let i=o.map(e=>e.index+1).join(', ');t.textContent=`Invalid format on line(s): ${i}`,e.style.color='red'}}";
231+
232+
const char ble_script[] = "";
233+
#endif
234+
222235
const char footer[] = "<div style='text-align:right;font-size:11px;'><hr/><a href='https://community.openmqttgateway.com' target='_blank' style='color:#aaa;'>%s</a></div></div></body></html>";
223236

224237
// Source file - https://github.com/1technophile/OpenMQTTGateway/blob/54decb4b65c7894b926ac3a89de0c6b2a3021506/docs/.vuepress/public/favicon-16x16.png

main/gatewayBT.cpp

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ BTConfig_s BTConfig;
5757

5858
# if BLEDecoder
5959
# include <decoder.h>
60+
# if BLEDecryptor
61+
# include "mbedtls/ccm.h"
62+
# include "mbedtls/aes.h"
63+
# endif
6064
TheengsDecoder decoder;
6165
# endif
6266

@@ -1154,6 +1158,27 @@ void launchBTDiscovery(bool overrideDiscovery) {
11541158
void launchBTDiscovery(bool overrideDiscovery) {}
11551159
# endif
11561160

1161+
# if BLEDecryptor
1162+
// ** TODO - Hex string to bytes, there is probably a function for this already just need to find it
1163+
int hexToBytes(String hex, uint8_t *out, size_t maxLen) {
1164+
int len = hex.length();
1165+
if (len % 2 || len / 2 > maxLen) return -1;
1166+
for (int i = 0, j = 0; i < len; i += 2, j++) {
1167+
out[j] = (uint8_t) strtol(hex.substring(i, i + 2).c_str(), nullptr, 16);
1168+
}
1169+
return len / 2;
1170+
}
1171+
// Reverse bytes
1172+
void reverseBytes(uint8_t *data, size_t length) {
1173+
size_t i;
1174+
for (i = 0; i < length / 2; i++) {
1175+
uint8_t temp = data[i];
1176+
data[i] = data[length - 1 - i];
1177+
data[length - 1 - i] = temp;
1178+
}
1179+
}
1180+
# endif
1181+
11571182
# if BLEDecoder
11581183
void process_bledata(JsonObject& BLEdata) {
11591184
yield(); // Necessary to let the loop run in case of connectivity issues
@@ -1166,6 +1191,213 @@ void process_bledata(JsonObject& BLEdata) {
11661191
int model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata);
11671192
int mac_type = BLEdata["mac_type"].as<int>();
11681193

1194+
# if BLEDecryptor
1195+
if (BLEdata["encr"] && (BLEdata["encr"].as<int>() >0 && BLEdata["encr"].as<int>() <=2)) {
1196+
// Decrypting Encrypted BLE Data PVVX, BTHome or Victron
1197+
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*>());
1198+
1199+
// MAC address
1200+
String macWOdots = BLEdata["id"].as<String>(); // Mac Address without dots
1201+
macWOdots.replace(":", "");
1202+
unsigned char macAddress[6];
1203+
int maclen = hexToBytes(macWOdots, macAddress, 6);
1204+
if (maclen != 6) {
1205+
Log.error(F("[BLEDecryptor] Invalid MAC Address length %d" CR), maclen);
1206+
return;
1207+
}
1208+
1209+
// AES decryption key
1210+
unsigned char bleaeskey[16];
1211+
int bleaeskeylength = 0;
1212+
if (ble_aes_keys.containsKey(macWOdots)){
1213+
Log.trace(F("[BLEDecryptor] Custom AES key %s" CR), ble_aes_keys[macWOdots].as<const char*>());
1214+
bleaeskeylength = hexToBytes(ble_aes_keys[macWOdots], bleaeskey, 16);
1215+
} else {
1216+
Log.trace(F("[BLEDecryptor] Default AES key" CR));
1217+
bleaeskeylength = hexToBytes(ble_aes, bleaeskey, 16);
1218+
}
1219+
// Check AES Key
1220+
if (bleaeskeylength != 16) {
1221+
Log.error(F("[BLEDecryptor] Invalid key length %d" CR), bleaeskeylength);
1222+
return;
1223+
}
1224+
1225+
// Build nonce and aad
1226+
uint8_t nonce[16];
1227+
int noncelength = 0;
1228+
unsigned char aad[1];
1229+
int aadLength;
1230+
1231+
if (BLEdata["encr"].as<int>() == 1){ // PVVX Encrypted
1232+
noncelength = 11; // 11 bytes
1233+
reverseBytes(macAddress, 6); // 6 bytes: device address in reverse
1234+
memcpy(nonce, macAddress, 6);
1235+
int maclen = hexToBytes(macWOdots, macAddress, 6);
1236+
1237+
unsigned char servicedata[16];
1238+
int servicedatalen = hexToBytes(BLEdata["servicedata"].as<String>(), servicedata, 16);
1239+
nonce[6] = servicedatalen + 3; // 1 byte : length of (service data + type and UUID)
1240+
nonce[7] = 0x16; // 1 byte : "16" -> AD type for "Service Data - 16-bit UUID"
1241+
nonce[8] = 0x1A; // 2 bytes: "1a18" -> UUID 181a in little-endian
1242+
nonce[9] = 0x18; //
1243+
unsigned char ctr[1]; // 1 byte : counter
1244+
int ctrlen = hexToBytes(BLEdata["ctr"].as<String>(), ctr, 1);
1245+
if (ctrlen != 1) {
1246+
Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen);
1247+
return;
1248+
}
1249+
nonce[10] = ctr[0];
1250+
aad[0] = 0x11;
1251+
aadLength = 1;
1252+
Log.trace(F("[BLEDecryptor] PVVX nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str());
1253+
1254+
} else if (BLEdata["encr"].as<int>() == 2){ // BTHome V2 Encrypted
1255+
noncelength = 13; // 13 bytes
1256+
memcpy(nonce, macAddress, 6);
1257+
nonce[6] = 0xD2; // UUID
1258+
nonce[7] = 0xFC;
1259+
nonce[8] = 0x41; // BTHome Device Data encrypted payload byte
1260+
unsigned char ctr[4]; // Counter
1261+
int ctrlen = hexToBytes(BLEdata["ctr"].as<String>(), ctr, 4);
1262+
if (ctrlen != 4) {
1263+
Log.error(F("[BLEDecryptor] Invalid counter length %d" CR), ctrlen);
1264+
return;
1265+
}
1266+
memcpy(&nonce[9], ctr, 4);
1267+
aad[0] = 0x00;
1268+
aadLength = 0;
1269+
Log.trace(F("[BLEDecryptor] BTHomeV2 nonce %s" CR), NimBLEUtils::dataToHexString(nonce, noncelength).c_str());
1270+
1271+
} else if (BLEdata["encr"].as<int>() == 3){
1272+
nonce[16] = {0}; // Victron has a 16 byte zero padded nonce with IV bytes 6,7
1273+
unsigned char iv[2];
1274+
int ivlen = hexToBytes(BLEdata["ctr"].as<String>(), iv, 2);
1275+
if (ivlen != 2) {
1276+
Log.error(F("[BLEDecryptor] Invalid iv length %d" CR), ivlen);
1277+
return;
1278+
}
1279+
memcpy(nonce, iv, 2);
1280+
Log.trace(F("[BLEDecryptor] Victron nonce %s" CR), NimBLEUtils::dataToHexString(nonce, 16).c_str());
1281+
} else {
1282+
return; // No match
1283+
}
1284+
1285+
// Ciphertext to bytes
1286+
int cipherlen = sizeof(BLEdata["cipher"].as<String>());
1287+
unsigned char ciphertext[cipherlen];
1288+
int ciphertextlen = hexToBytes(BLEdata["cipher"].as<String>(), ciphertext, cipherlen);
1289+
unsigned char decrypted[ciphertextlen]; // Decrypted payload
1290+
1291+
// Decrypt ciphertext
1292+
if (BLEdata["encr"].as<int>() == 1 || BLEdata["encr"].as<int>() == 2) {
1293+
// Decrypt PVVX and BTHome V2 ciphertext using AES CCM
1294+
mbedtls_ccm_context ctx;
1295+
mbedtls_ccm_init(&ctx);
1296+
if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, bleaeskey, 128) != 0) {
1297+
Log.error(F("[BLEDecryptor] Failed to set AES key to mbedtls" CR));
1298+
return;
1299+
}
1300+
1301+
// Message Integrity Check (MIC)
1302+
unsigned char mic[4];
1303+
int miclen = hexToBytes(BLEdata["mic"].as<String>(), mic, 4);
1304+
if (miclen != 4) {
1305+
Log.error(F("[BLEDecryptor] Invalid MIC length %d" CR), miclen);
1306+
return;
1307+
}
1308+
1309+
int ret = mbedtls_ccm_auth_decrypt(
1310+
&ctx, // AES Key
1311+
ciphertextlen, // length of ciphertext
1312+
nonce, noncelength, // Nonce
1313+
aad, aadLength, // AAD
1314+
ciphertext, // input ciphertext
1315+
decrypted, // output plaintext
1316+
mic, sizeof(mic) // Message Integrity Check
1317+
);
1318+
mbedtls_ccm_free(&ctx);
1319+
1320+
if (ret == 0) {
1321+
Log.notice(F("[BLEDecryptor] Decryption successful" CR));
1322+
} else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) {
1323+
Log.error(F("[BLEDecryptor] Authentication failed." CR));
1324+
return;
1325+
} else {
1326+
Log.error(F("[BLEDecryptor] Decryption failed with error: %X" CR), ret);
1327+
return;
1328+
}
1329+
1330+
// Build new servicedata
1331+
if (BLEdata["encr"].as<int>() == 1){ // PVVX
1332+
BLEdata["servicedata"] = NimBLEUtils::dataToHexString(decrypted, ciphertextlen);
1333+
} else if (BLEdata["encr"].as<int>() == 2) { // BTHomeV2
1334+
// Build new servicedata
1335+
uint8_t newservicedata[3 + ciphertextlen];
1336+
newservicedata[0] = 0x40; // Decrypted BTHomeV2 Packet Type
1337+
newservicedata[1] = 0x00; // Packet counter which the PVVX BTHome non-encrypted has but the encrypted does not
1338+
newservicedata[2] = 0x00; // **TODO Convert the ctr to the packet counter or just stick with 0?
1339+
memcpy(&newservicedata[3], decrypted, ciphertextlen);
1340+
BLEdata["servicedata"] = NimBLEUtils::dataToHexString(newservicedata, ciphertextlen + 3);
1341+
} else {
1342+
return;
1343+
}
1344+
Log.trace(F("[BLEDecryptor] Decrypted servicedata %s" CR), BLEdata["servicedata"].as<const char*>());
1345+
1346+
} else if (BLEdata["encr"].as<int>() == 3) {
1347+
// Decrypt Victron Energy encrypted advertisements.
1348+
size_t nc_off = 0;
1349+
uint8_t stream_block[16] = {0};
1350+
1351+
mbedtls_aes_context ctx;
1352+
mbedtls_aes_init(&ctx);
1353+
mbedtls_aes_setkey_enc(&ctx, bleaeskey, 128);
1354+
int ret = mbedtls_aes_crypt_ctr(
1355+
&ctx, // AES Key
1356+
ciphertextlen, // length of ciphertext
1357+
&nc_off,
1358+
nonce, // 16 byte nonce with 2 bytes iv
1359+
stream_block,
1360+
ciphertext, // input ciphertext
1361+
decrypted // output plaintext
1362+
);
1363+
mbedtls_aes_free(&ctx);
1364+
1365+
if (ret == 0) {
1366+
Log.notice(F("[BLEDecryptor] Victron Decryption successful" CR));
1367+
} else if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED) {
1368+
Log.error(F("[BLEDecryptor] Victron Authentication failed." CR));
1369+
return;
1370+
} else {
1371+
Log.error(F("[BLEDecryptor] Victron decryption failed with error: %X" CR), ret);
1372+
return;
1373+
}
1374+
1375+
// Build new manufacturerdata
1376+
unsigned char manufacturerdata[10 + ciphertextlen];
1377+
int manufacturerdatalen = hexToBytes(BLEdata["manufacturerdata"].as<String>(), manufacturerdata, 10);
1378+
manufacturerdata[2] = 0x11; // Replace byte 2 with "11" indicate decrypted data
1379+
manufacturerdata[7] = 0xff; // Replace byte 7 with "ff" to indicate decrypted data
1380+
manufacturerdata[8] = 0xff; // Replace byte 8 with "ff" to indicate decrypted data
1381+
memcpy(&manufacturerdata[8], decrypted, ciphertextlen); // Append the decrypted payload to the manufacturer data
1382+
BLEdata["manufacturerdata"] = NimBLEUtils::dataToHexString(manufacturerdata, 10 + ciphertextlen); // Rebuild manufacturerdata
1383+
Log.trace(F("[BLEDecryptor] Victron decrypted manufacturerdata %s" CR), BLEdata["manufacturerdata"].as<const char*>());
1384+
}
1385+
1386+
// Print before and after decoder post decryption
1387+
// serializeJsonPretty(BLEdata, Serial);
1388+
model_id = BTConfig.extDecoderEnable ? -1 : decoder.decodeBLEJson(BLEdata);
1389+
// serializeJsonPretty(BLEdata, Serial);
1390+
Log.trace(F("[BLEDecryptor] Decrypted model_id %d" CR), model_id);
1391+
1392+
// Remove the cipher fields from BLEdata
1393+
BLEdata.remove("encr");
1394+
BLEdata.remove("cipher");
1395+
BLEdata.remove("ctr");
1396+
BLEdata.remove("mic");
1397+
1398+
}
1399+
# endif
1400+
11691401
// Convert prmacs to RMACS until or if OMG gets Identity MAC/IRK decoding
11701402
if (BLEdata["prmac"]) {
11711403
BLEdata.remove("prmac");

main/main.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ bool ethConnected = false;
9494
char mqtt_topic[parameters_size + 1] = Base_Topic;
9595
char gateway_name[parameters_size + 1] = Gateway_Name;
9696
unsigned long lastDiscovery = 0;
97+
98+
#ifdef BLEDecryptor
99+
char ble_aes[parameters_size] = BLE_AES;
100+
StaticJsonDocument<JSON_BLE_AES_CUSTOM_KEYS> ble_aes_keys;
101+
#endif
102+
97103
#if !MQTT_BROKER_MODE
98104
ss_cnt_parameters cnt_parameters_array[cnt_parameters_array_size] = CNT_PARAMS_ARR;
99105
#endif
@@ -2022,6 +2028,10 @@ void saveConfig() {
20222028
# endif
20232029
json["gateway_name"] = gateway_name;
20242030
json["ota_pass"] = ota_pass;
2031+
# ifdef BLEDecryptor
2032+
json["ble_aes"] = ble_aes;
2033+
json["ble_aes_keys"] = ble_aes_keys;
2034+
# endif
20252035

20262036
File configFile = SPIFFS.open("/config.json", "w");
20272037
if (!configFile) {
@@ -2169,6 +2179,16 @@ bool loadConfigFromFlash() {
21692179
}
21702180
# endif
21712181
}
2182+
# ifdef BLEDecryptor
2183+
if (json.containsKey("ble_aes")) {
2184+
strcpy(ble_aes, json["ble_aes"]);
2185+
Log.trace(F("loaded default BLE AES key %s" CR), ble_aes);
2186+
}
2187+
if (json.containsKey("ble_aes_keys")) {
2188+
ble_aes_keys = json["ble_aes_keys"];
2189+
Log.trace(F("loaded %d custom BLE AES keys" CR), ble_aes_keys.size());
2190+
}
2191+
# endif
21722192
result = true;
21732193
} else {
21742194
Log.warning(F("failed to load json config" CR));
@@ -3441,6 +3461,25 @@ void XtoSYS(const char* topicOri, JsonObject& SYSdata) { // json object decoding
34413461
mqttSetupPending = true; // trigger reconnect in loop using the new topic/name
34423462
}
34433463

3464+
#ifdef BLEDecryptor
3465+
if (SYSdata.containsKey("ble_aes") || SYSdata.containsKey("ble_aes_keys")) {
3466+
if (SYSdata.containsKey("ble_aes")) {
3467+
strncpy(ble_aes, SYSdata["ble_aes"], parameters_size);
3468+
}
3469+
if (SYSdata.containsKey("ble_aes_keys")) {
3470+
Log.warning(F("Contains ble_aes_keys" CR));
3471+
ble_aes_keys = SYSdata["ble_aes_keys"];
3472+
} else {
3473+
// If no keys are passed clear the object.
3474+
StaticJsonDocument<JSON_BLE_AES_CUSTOM_KEYS> jsonBLEBuffer;
3475+
ble_aes_keys = jsonBLEBuffer.to<JsonObject>();
3476+
}
3477+
# ifndef ESPWifiManualSetup
3478+
saveConfig();
3479+
# endif
3480+
}
3481+
#endif
3482+
34443483
#if !MQTT_BROKER_MODE
34453484
# ifdef MQTTsetMQTT
34463485

0 commit comments

Comments
 (0)