mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
[bthome_mithermometer] add encrypted beacon support (#13428)
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble_tracker
|
from esphome.components import esp32_ble_tracker
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
|
from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS
|
||||||
|
from esphome.core import HexInt
|
||||||
|
|
||||||
CODEOWNERS = ["@nagyrobi"]
|
CODEOWNERS = ["@nagyrobi"]
|
||||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||||
@@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None):
|
|||||||
{
|
{
|
||||||
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
||||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||||
|
cv.Optional(CONF_BINDKEY): cv.bind_key,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(BLE_DEVICE_SCHEMA)
|
.extend(BLE_DEVICE_SCHEMA)
|
||||||
@@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config):
|
|||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await esp32_ble_tracker.register_ble_device(var, config)
|
await esp32_ble_tracker.register_ble_device(var, config)
|
||||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||||
|
if bindkey := config.get(CONF_BINDKEY):
|
||||||
|
bindkey_bytes = [
|
||||||
|
HexInt(int(bindkey[index : index + 2], 16))
|
||||||
|
for index in range(0, len(bindkey), 2)
|
||||||
|
]
|
||||||
|
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))
|
||||||
|
|||||||
@@ -3,15 +3,23 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include "mbedtls/ccm.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace bthome_mithermometer {
|
namespace bthome_mithermometer {
|
||||||
|
|
||||||
static const char *const TAG = "bthome_mithermometer";
|
static const char *const TAG = "bthome_mithermometer";
|
||||||
|
static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
|
||||||
|
static constexpr size_t BTHOME_NONCE_SIZE = 13;
|
||||||
|
static constexpr size_t BTHOME_MIC_SIZE = 4;
|
||||||
|
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
|
||||||
|
|
||||||
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
|
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
|
||||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||||
@@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() {
|
|||||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
||||||
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
|
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
|
||||||
|
if (this->has_bindkey_) {
|
||||||
|
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
|
||||||
|
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
|
||||||
|
}
|
||||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||||
@@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
|
|||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
|
||||||
|
if (bindkey.size() != sizeof(this->bindkey_)) {
|
||||||
|
ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
|
||||||
|
this->has_bindkey_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
|
||||||
|
std::vector<uint8_t> &payload) const {
|
||||||
|
if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
|
||||||
|
ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
|
||||||
|
payload.resize(ciphertext_size);
|
||||||
|
|
||||||
|
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||||
|
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||||
|
mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
|
||||||
|
memcpy(nonce.data(), mac.data(), mac.size());
|
||||||
|
nonce[6] = 0xD2;
|
||||||
|
nonce[7] = 0xFC;
|
||||||
|
nonce[8] = data[0];
|
||||||
|
memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
|
||||||
|
|
||||||
|
const uint8_t *ciphertext = data.data() + 1;
|
||||||
|
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
|
||||||
|
|
||||||
|
mbedtls_ccm_context ctx;
|
||||||
|
mbedtls_ccm_init(&ctx);
|
||||||
|
|
||||||
|
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
|
||||||
|
if (ret) {
|
||||||
|
ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
|
||||||
|
mbedtls_ccm_free(&ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
|
||||||
|
payload.data(), mic, BTHOME_MIC_SIZE);
|
||||||
|
mbedtls_ccm_free(&ctx);
|
||||||
|
if (ret) {
|
||||||
|
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||||
const esp32_ble_tracker::ESPBTDevice &device) {
|
const esp32_ble_tracker::ESPBTDevice &device) {
|
||||||
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
||||||
@@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
uint64_t source_address = device.address_uint64();
|
||||||
if (is_encrypted) {
|
bool address_matches = source_address == this->address_;
|
||||||
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
|
if (!is_encrypted && mac_included && data.size() >= 7) {
|
||||||
|
uint64_t advertised_address = 0;
|
||||||
|
for (int i = 5; i >= 0; i--) {
|
||||||
|
advertised_address = (advertised_address << 8) | data[1 + i];
|
||||||
|
}
|
||||||
|
address_matches = address_matches || advertised_address == this->address_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_encrypted && !this->has_bindkey_) {
|
||||||
|
if (address_matches) {
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
|
ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
|
||||||
|
device.address_str_to(addr_buf));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t payload_index = 1;
|
if (!is_encrypted && this->has_bindkey_) {
|
||||||
uint64_t source_address = device.address_uint64();
|
if (address_matches) {
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
|
ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
|
||||||
|
device.address_str_to(addr_buf));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> decrypted_payload;
|
||||||
|
const uint8_t *payload = nullptr;
|
||||||
|
size_t payload_size = 0;
|
||||||
|
|
||||||
|
if (is_encrypted) {
|
||||||
|
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
|
ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
payload = decrypted_payload.data();
|
||||||
|
payload_size = decrypted_payload.size();
|
||||||
|
} else {
|
||||||
|
payload = data.data() + 1;
|
||||||
|
payload_size = data.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (mac_included) {
|
if (mac_included) {
|
||||||
if (data.size() < 7) {
|
if (payload_size < 6) {
|
||||||
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
source_address = 0;
|
source_address = 0;
|
||||||
for (int i = 5; i >= 0; i--) {
|
for (int i = 5; i >= 0; i--) {
|
||||||
source_address = (source_address << 8) | data[1 + i];
|
source_address = (source_address << 8) | payload[i];
|
||||||
}
|
}
|
||||||
payload_index = 7;
|
payload += 6;
|
||||||
|
payload_size -= 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
if (source_address != this->address_) {
|
if (source_address != this->address_) {
|
||||||
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload_index >= data.size()) {
|
if (payload_size == 0) {
|
||||||
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool reported = false;
|
bool reported = false;
|
||||||
size_t offset = payload_index;
|
size_t offset = 0;
|
||||||
uint8_t last_type = 0;
|
uint8_t last_type = 0;
|
||||||
|
|
||||||
while (offset < data.size()) {
|
while (offset < payload_size) {
|
||||||
const uint8_t obj_type = data[offset++];
|
const uint8_t obj_type = payload[offset++];
|
||||||
size_t value_length = 0;
|
size_t value_length = 0;
|
||||||
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
||||||
|
|
||||||
if (has_length_byte) {
|
if (has_length_byte) {
|
||||||
if (offset >= data.size()) {
|
if (offset >= payload_size) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
value_length = data[offset++];
|
value_length = payload[offset++];
|
||||||
} else {
|
} else {
|
||||||
if (!get_bthome_value_length(obj_type, value_length)) {
|
if (!get_bthome_value_length(obj_type, value_length)) {
|
||||||
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
||||||
@@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset + value_length > data.size()) {
|
if (offset + value_length > payload_size) {
|
||||||
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t *value = &data[offset];
|
const uint8_t *value = &payload[offset];
|
||||||
offset += value_length;
|
offset += value_length;
|
||||||
|
|
||||||
if (obj_type < last_type) {
|
if (obj_type < last_type) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ namespace bthome_mithermometer {
|
|||||||
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||||
public:
|
public:
|
||||||
void set_address(uint64_t address) { this->address_ = address; }
|
void set_address(uint64_t address) { this->address_ = address; }
|
||||||
|
void set_bindkey(std::initializer_list<uint8_t> bindkey);
|
||||||
|
|
||||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||||
@@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
|
|||||||
protected:
|
protected:
|
||||||
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||||
const esp32_ble_tracker::ESPBTDevice &device);
|
const esp32_ble_tracker::ESPBTDevice &device);
|
||||||
|
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
|
||||||
|
std::vector<uint8_t> &payload) const;
|
||||||
|
|
||||||
uint64_t address_{0};
|
uint64_t address_{0};
|
||||||
optional<uint8_t> last_packet_id_{};
|
optional<uint8_t> last_packet_id_{};
|
||||||
|
bool has_bindkey_{false};
|
||||||
|
uint8_t bindkey_[16];
|
||||||
|
|
||||||
sensor::Sensor *temperature_{nullptr};
|
sensor::Sensor *temperature_{nullptr};
|
||||||
sensor::Sensor *humidity_{nullptr};
|
sensor::Sensor *humidity_{nullptr};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ esp32_ble_tracker:
|
|||||||
sensor:
|
sensor:
|
||||||
- platform: bthome_mithermometer
|
- platform: bthome_mithermometer
|
||||||
mac_address: A4:C1:38:4E:16:78
|
mac_address: A4:C1:38:4E:16:78
|
||||||
|
bindkey: eef418daf699a0c188f3bfd17e4565d9
|
||||||
temperature:
|
temperature:
|
||||||
name: "BTHome Temperature"
|
name: "BTHome Temperature"
|
||||||
humidity:
|
humidity:
|
||||||
|
|||||||
Reference in New Issue
Block a user