[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:
1 parent
a0d5b7d181
commit
2d29ee4524
7 files changed
+399
-1
No files matched your search
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user