前言
(1)因为需要将
ESP32
利用BTHome
接入Home Assistant
,所以我需要先了解一下BTHome
的数据格式。
BLE广播包格式
固定格式数据解析
(1)
BTHome
是依赖BLE
的广播功能进行的一个通用数据格式。因此,我们在学习BTHome
之前,需要先简单了解一下BLE
的广播格式。
- preamble(1/2 byte) : 这个前导码可以用于同步通讯双方的时钟,为接受方提供正确的时间基准。如果是
2M PHY
, 那么前导码为2byte
。如果是1M PHY
, 那么前导码为1byte
。- Access Address(4 byte) : 接入地址,广播时地址固定为 0x8e89bed6 。当两台设备连接建立时,将会分配一个符合白化条件的固定地址,该地址用于标识一个连接。
- PDU Header(1 byte) : 这个用于指示当前广播数据的一些特性。
- PDU Type(4 bit) : 这个用于指示什么类型的广播数据。
- RFU(1 bit) : 留作将来使用。
- ChSel(1 bit) : 如果广播设备支持
LE Channel Selection Algorithm #2
算法,该位会被置1。这位会影响BLE
设备广播时使用的通道选择策略,使能该算法能提高广播包的传输成功率或降低能耗。- TxAdd(1 bit) : 如果广播设备为随机地址,该位置1。如果是公共地址,该位置0。
- RxAdd(1 bit) : 如果扫描设备为随机地址,该位置1。如果是公共地址,该位置0。
- Length(1 byte) : 有效载荷(Payload)数据长度。
- Payload(1-255 byte) : 实际的有效载荷,后面进一步介绍。
- CRC(3 byte) : 用于数据校验。
有效载荷(Payload)数据解析
(1)现在我们看重点了,有效载荷(
Payload
)中分为一个一个的AD Structure
,每一个AD Structure
代表一种类型数据,例如设备地址、发射功率、设备地址等信息。
(2)AD Structure
又可以拆分为如下三种数据:
- Length(1 byte) : 说明AD Type + AD Data 的长度,单位是
byte
- AD Type(1 byte) : 这个用于指示当前
AD Structure
数据是表示什么含义。其中需要注意,一定包含一个0x01
类型的数据,该数据表示Flag
,用于指示广播设备是对BLE
和经典蓝牙是否支持。关于该更为详细的AD Type 定义,可以阅读Assigned Numbers Document (PDF)的2.3 Common Data Types章节。- AD Data( Length - 1 byte) : 这个就是实际的数据了。具体数据含义与AD Type有关。
明文
明文格式
(1)现在拥有了上面的基础,我们来看看
BTHome
的数据格式应该是怎么样的呢?我们现在开始看下面的这段,假设我们有一个名字叫做ADC
的BLE
设备广播BTHome
数据,那么他的数据格式应当如下。
- AD Structure 1 : 因为广播数据必须包含
0x01
数据,该数据表示当前设备为 LE 通用可发现模式且不支持经典蓝牙。- AD Structure 2 :
0x09
表示当前数据为设备名字,通过ASCII
码表我们可知,当前设备的名字为ADC
。- AD Structure 3 : 接下来是
BTHome
数据。
0x0A
: 用于指示当前AD Structure
长度。这个因BTHome
数据内容长度多少而改变,与之前的含义一样。0x16
: 表示当前数据是自定义的16
位服务UUID
。正式因为BLE
能够自定义服务,因此使其有较强的扩展性。从而能够有BTHome
这种数据包。0xD2FC
:Shelly
设备的制造商Allterco Robotics
为BTHome
赞助了这个UUID
。因此,当BTHome
设备识别到是自定义的16
位服务时候,就会看接下来的2 byte
数据是否为0xD2FC
如果是的说明当前数据为BTHome
数据。- BTHome Device Information : 该数据主要用于指示当前设备的是否为加密数据、是否定期更新数据、BTHome 版本 。
- BTHome DATA : 这个就是实际的
BTHome
数据了。
(2)BTHome Device Information的具体内容如下:
- 加密标志位(Encryption flag) :
bit0
,这个参数用于指示当前设备是否为加密数据。0表示为非加密数据,1表示为加密数据。- 触发标志位(Trigger based device flag) :
bit2
,该位用于告知接收器该设备是发送定期数据更新还是仅在触发事件时发送数据。这对接收器来说可能是有用的信息,例如可以防止设备进入不可用状态。目前本人还没具体搞明白该位有啥用。- BTHome 版本 :
bit 5-7
,代表BTHome
版本。目前仅允许使用BTHome
版本 1 或 2,其中 2 是最新版本。bit 1
、bit 3-4
: 保留(3)BTHome DATA理解:假设我们有16位
UUID
数据0xD2FC4002C40903BF13
,根据前面的介绍。我们知道这是一个BTHome
的非加密数据。因此我们可以尝试进行解析该数据。首先剔除非BTHome DATA的数据,得到实际有效数据为02C40903BF13
。BTHome
数据含义可以参考BTHome BLE advertising in the BTHome v2 format链接。
0x02 C409
:0x02
表示温度数据,接下来的2个字节表示温度数据,单位为0.01℃
。因为当前是0xC409
,而BTHome
是小端存储因此实际数据应该是0x09C4
,转换为十进制就是2500
,因此当前温度为25℃
。0x03 BF13
:0x02
表示当前是湿度数据,系数为0.01
。同理,小段存储,当前实际数据应该是0x13BF
转换为十进制就是5055
,因此当前湿度为50.55%
。
明文可能存在的问题
(1)假设当前存在一个
BLE
设备是一个按键。我每次通过按下按键来控制LED
的开关。那么此时存在一个问题,因为BLE
进行广播时,为了保证数据被对端设备接收到,一个相同的数据包会进行多次广播。因此,对端设备也会采取相应的过滤机制,当他收到一个数据包时,发现下一个数据包和当前数据包一样,那么就会过滤掉该数据包。
(2)这样就存在一个问题,我每次按键按下时,发送的数据包都是一样的,那么我的按键将永远只有第一次按下才能够被对端设备识别到。
(3)为了解决这个问题,我们可以采用0x00
类型数据。例如当我第一次按下按键,数据为0x0000 3A01
,第二次按下数据为0x0001 3A01
。
加密数据
加密数据介绍
(1)一般来说,我们发送数据可能希望只被特定设备接收到,其余设备即使接收到也无法获知当前设备含义,于是我们可以用上数据包加密。
(2)数据包加密存在两个好处:
<1>其余设备即使截取到当前广播信息,也无法获知数据含义。
<2>即使是相同的数据,因为时间戳不同,加密后每次数据也不一样。因此不会存在数据包被错误过滤问题。
(3)注意,加密数据的BTHome Device Information的bit 0
要设置为1。
解密脚本
(1)如下为一个数据包解密脚本。
import binascii
from cryptography.hazmat.primitives.ciphers.aead import AESCCM
def decrypt_payload(payload: bytes, mic: bytes, key: bytes, nonce: bytes) -> bytes:
"""Decrypt payload."""
cipher = AESCCM(key, tag_length=4)
try:
data = cipher.decrypt(nonce, payload + mic, None)
except ValueError as error:
print("Decryption failed:", error)
return b''
return data
def decrypt_aes_ccm(key: bytes, mac: bytes, data: bytes) -> bytes:
"""Decrypt AES CCM."""
if len(data) > 15 and data[0] == 0xD2 and data[1] == 0xFC:
pkt = data[: data[0] + 1]
uuid = pkt[0:2]
sw_version = pkt[2:3]
encrypted_data = pkt[3:-8]
count_id = pkt[-8:-4]
mic = pkt[-4:]
# nonce: mac [6], uuid16 [2], count_id [4] # 6+3+4 = 13 bytes
nonce = b"".join([mac, uuid, sw_version, count_id])
return decrypt_payload(encrypted_data, mic, key, nonce)
else:
print("Error: format packet!")
return b''
def main():
"""Example to decrypt BTHome payload."""
# Input encrypted data and key
encrypted_data_hex = input("Enter encrypted BTHome data (hex): ")
key_hex = input("Enter key (hex): ")
mac_hex = input("Enter MAC address (hex): ")
# Convert inputs to bytes
key = binascii.unhexlify(key_hex)
mac = binascii.unhexlify(mac_hex)
data = binascii.unhexlify(encrypted_data_hex)
# Decrypt the data
decrypted_data = decrypt_aes_ccm(key=key, mac=mac, data=data)
if decrypted_data:
print("Decrypted Hex Data:", decrypted_data.hex())
else:
print("Decryption failed")
if __name__ == "__main__":
main()
(2)文件保存方法
# 1. 创建一个 decrypt_bthome.py 脚本
vim decrypt_bthome.py
# 2. 输入i,将如上代码粘贴进来
# 3. 输入 :wq 保存
# 4. 将脚本设置为可执行文件
sudo chmod +x decrypt_bthome.py
(3)示例输入
# 执行脚本
python3 decrypt_bthome.py
# 输入加密后的 BTHome 数据
Enter encrypted BTHome data (hex): d2fc41a47266c95f730011223378237214
# 输入密钥
Enter key (hex): 231d39c1d7cc1ab1aee224cd096db932
# 输入广播设备的 MAC 地址
Enter MAC address (hex): 5448E68F80A5
(4)示例输出
# 解密后的数据
Decrypted Hex Data: 02ca0903bf13
参考
(1)BTHome官方解密示例脚本
(2)BTHome encryption
(3)BTHome BLE advertising in the BTHome v2 format
(5)Core Specification_v5.0
(6)C站:BLE学习(2):广播包报文格式详解
(7)Assigned Numbers Document (PDF)