diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py index 0e84278afa..8ce216da22 100644 --- a/esphome/components/bthome_mithermometer/__init__.py +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg from esphome.components import esp32_ble_tracker 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"] 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.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_BINDKEY): cv.bind_key, } ) .extend(BLE_DEVICE_SCHEMA) @@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) 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))) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp index d1c5165896..2b73d8735c 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.cpp +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -3,15 +3,23 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include +#include #include #ifdef USE_ESP32 +#include "mbedtls/ccm.h" + namespace esphome { namespace 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 buffer, uint64_t address) { std::array mac{}; @@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() { char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); 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(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev return matched; } +void BTHomeMiThermometer::set_bindkey(std::initializer_list 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 &data, uint64_t source_address, + std::vector &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 mac{}; + for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) { + mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF; + } + + std::array 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, const esp32_ble_tracker::ESPBTDevice &device) { if (!service_data.uuid.contains(0xD2, 0xFC)) { @@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD return false; } - char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - if (is_encrypted) { - ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf)); + uint64_t source_address = device.address_uint64(); + bool address_matches = source_address == this->address_; + 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; } - size_t payload_index = 1; - uint64_t source_address = device.address_uint64(); + if (!is_encrypted && this->has_bindkey_) { + 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 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 (data.size() < 7) { + if (payload_size < 6) { ESP_LOGVV(TAG, "BTHome payload missing MAC address"); return false; } source_address = 0; 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_) { ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address)); return false; } - if (payload_index >= data.size()) { + if (payload_size == 0) { ESP_LOGVV(TAG, "BTHome payload empty after header"); return false; } bool reported = false; - size_t offset = payload_index; + size_t offset = 0; uint8_t last_type = 0; - while (offset < data.size()) { - const uint8_t obj_type = data[offset++]; + while (offset < payload_size) { + const uint8_t obj_type = payload[offset++]; size_t value_length = 0; bool has_length_byte = obj_type == 0x53; // text objects include explicit length if (has_length_byte) { - if (offset >= data.size()) { + if (offset >= payload_size) { break; } - value_length = data[offset++]; + value_length = payload[offset++]; } else { if (!get_bthome_value_length(obj_type, value_length)) { 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; } - if (offset + value_length > data.size()) { + if (offset + value_length > payload_size) { ESP_LOGVV(TAG, "BTHome object length exceeds payload"); break; } - const uint8_t *value = &data[offset]; + const uint8_t *value = &payload[offset]; offset += value_length; if (obj_type < last_type) { diff --git a/esphome/components/bthome_mithermometer/bthome_ble.h b/esphome/components/bthome_mithermometer/bthome_ble.h index 3d2380b48d..ef3038ec93 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.h +++ b/esphome/components/bthome_mithermometer/bthome_ble.h @@ -5,6 +5,8 @@ #include "esphome/core/component.h" #include +#include +#include #ifdef USE_ESP32 @@ -14,6 +16,7 @@ namespace bthome_mithermometer { class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(std::initializer_list bindkey); void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } @@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi protected: bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, const esp32_ble_tracker::ESPBTDevice &device); + bool decrypt_bthome_payload_(const std::vector &data, uint64_t source_address, + std::vector &payload) const; uint64_t address_{0}; optional last_packet_id_{}; + bool has_bindkey_{false}; + uint8_t bindkey_[16]; sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/tests/components/bthome_mithermometer/common.yaml b/tests/components/bthome_mithermometer/common.yaml index ba94e46878..7a68fae966 100644 --- a/tests/components/bthome_mithermometer/common.yaml +++ b/tests/components/bthome_mithermometer/common.yaml @@ -3,6 +3,7 @@ esp32_ble_tracker: sensor: - platform: bthome_mithermometer mac_address: A4:C1:38:4E:16:78 + bindkey: eef418daf699a0c188f3bfd17e4565d9 temperature: name: "BTHome Temperature" humidity: