Coverage for src / bluetooth_sig / types / mesh.py: 60%
147 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Bluetooth Mesh protocol types per Bluetooth Mesh Profile Specification.
3This module contains types for Bluetooth Mesh protocol structures including
4beacon types, provisioning PDUs, and mesh network data.
6Reference: Bluetooth Mesh Profile Specification v1.1
7"""
9from __future__ import annotations
11import secrets
12import struct
13from enum import IntEnum, IntFlag
15import msgspec
17# =============================================================================
18# Bluetooth Mesh Protocol Sizes (bytes)
19# =============================================================================
20NETWORK_ID_LENGTH = 8
21NETWORK_KEY_LENGTH = 16
22DEVICE_UUID_LENGTH = 16
23AUTHENTICATION_VALUE_LENGTH = 8
24OOB_INFO_LENGTH = 2
25URI_HASH_LENGTH = 4
26UNPROVISIONED_BEACON_MIN_LENGTH = DEVICE_UUID_LENGTH + OOB_INFO_LENGTH
27UNPROVISIONED_BEACON_WITH_HASH_LENGTH = UNPROVISIONED_BEACON_MIN_LENGTH + URI_HASH_LENGTH
29# Network MIC lengths per Bluetooth Mesh Profile 3.4.3
30ACCESS_MESSAGE_MIC_LENGTH = 4
31CONTROL_MESSAGE_MIC_LENGTH = 8
33# Mesh Provisioning algorithm constants
34ALGORITHM_FIPS_P256 = 0x0001
35PUBLIC_KEY_TYPE_NONE = 0x00
36STATIC_OOB_TYPE_NONE = 0x00
38# =============================================================================
39# Enums and Flags
40# =============================================================================
43class SecureNetworkBeaconFlags(IntFlag):
44 """Secure Network Beacon flags per Bluetooth Mesh Profile 3.10.3.
46 These flags are combined in the beacon's flags byte.
47 """
49 NONE = 0x00
50 KEY_REFRESH = 0x01
51 IV_UPDATE = 0x02
54class MeshBeaconType(IntEnum):
55 """Mesh beacon types per Bluetooth Mesh Profile 3.10.
57 These identify the type of mesh beacon being broadcast.
58 """
60 UNPROVISIONED_DEVICE = 0x00
61 SECURE_NETWORK = 0x01
64class ProvisioningPDUType(IntEnum):
65 """Mesh Provisioning PDU types per Bluetooth Mesh Profile 5.4.
67 These identify the provisioning protocol message type.
68 """
70 INVITE = 0x00
71 CAPABILITIES = 0x01
72 START = 0x02
73 PUBLIC_KEY = 0x03
74 INPUT_COMPLETE = 0x04
75 CONFIRMATION = 0x05
76 RANDOM = 0x06
77 DATA = 0x07
78 COMPLETE = 0x08
79 FAILED = 0x09
82class MeshCapabilities(msgspec.Struct, frozen=True, kw_only=True):
83 """Mesh Provisioning Capabilities per Bluetooth Mesh Profile 5.4.1.2.
85 This structure describes the device's provisioning capabilities
86 and is sent in response to a Provisioning Invite PDU.
88 Attributes:
89 num_elements: Number of elements supported by the device
90 algorithms: Supported provisioning algorithms (bitmask)
91 public_key_type: Supported public key types
92 static_oob_type: Supported static OOB types
93 output_oob_size: Maximum size of Output OOB
94 output_oob_action: Supported Output OOB actions
95 input_oob_size: Maximum size of Input OOB
96 input_oob_action: Supported Input OOB actions
98 """
100 num_elements: int = 1
101 algorithms: int = ALGORITHM_FIPS_P256
102 public_key_type: int = PUBLIC_KEY_TYPE_NONE
103 static_oob_type: int = STATIC_OOB_TYPE_NONE
104 output_oob_size: int = 0
105 output_oob_action: int = 0
106 input_oob_size: int = 0
107 input_oob_action: int = 0
109 def encode(self) -> bytearray:
110 """Encode capabilities to provisioning PDU format."""
111 return bytearray(
112 struct.pack(
113 "<BBHBBBBBB",
114 ProvisioningPDUType.CAPABILITIES,
115 self.num_elements,
116 self.algorithms,
117 self.public_key_type,
118 self.static_oob_type,
119 self.output_oob_size,
120 self.output_oob_action,
121 self.input_oob_size,
122 self.input_oob_action,
123 )
124 )
127class SecureNetworkBeacon(msgspec.Struct, frozen=True, kw_only=True):
128 """Mesh Secure Network Beacon per Bluetooth Mesh Profile 3.10.3.
130 This beacon is broadcast by provisioned nodes to announce
131 network presence and IV index.
133 Attributes:
134 network_id: 8-byte Network ID derived from network key
135 iv_index: Current IV Index
136 key_refresh_flag: Key Refresh Flag
137 iv_update_flag: IV Update Flag
138 authentication_value: 8-byte authentication value
140 """
142 network_id: bytes # 8 bytes, derived from network key
143 iv_index: int = 0
144 key_refresh_flag: bool = False
145 iv_update_flag: bool = False
146 authentication_value: bytes = b"" # 8 bytes when parsed
148 def encode(self) -> bytearray:
149 """Encode to beacon format.
151 Returns:
152 Encoded beacon bytes
154 Raises:
155 ValueError: If network_id is not 8 bytes
157 """
158 if len(self.network_id) != NETWORK_ID_LENGTH:
159 msg = f"network_id must be {NETWORK_ID_LENGTH} bytes, got {len(self.network_id)}"
160 raise ValueError(msg)
162 flags = SecureNetworkBeaconFlags.NONE
163 if self.key_refresh_flag:
164 flags |= SecureNetworkBeaconFlags.KEY_REFRESH
165 if self.iv_update_flag:
166 flags |= SecureNetworkBeaconFlags.IV_UPDATE
168 # Use provided auth value or generate random for encoding
169 auth_value = (
170 self.authentication_value
171 if len(self.authentication_value) == AUTHENTICATION_VALUE_LENGTH
172 else secrets.token_bytes(AUTHENTICATION_VALUE_LENGTH)
173 )
175 return bytearray(
176 struct.pack(
177 "<BB8sI8s",
178 MeshBeaconType.SECURE_NETWORK,
179 flags,
180 self.network_id,
181 self.iv_index,
182 auth_value,
183 )
184 )
186 @classmethod
187 def decode(cls, data: bytes | bytearray) -> SecureNetworkBeacon:
188 """Decode from beacon bytes.
190 Args:
191 data: Raw beacon bytes (without beacon type byte)
193 Returns:
194 Parsed SecureNetworkBeacon
196 Raises:
197 ValueError: If data is too short
199 """
200 min_length = 1 + NETWORK_ID_LENGTH + 4 + AUTHENTICATION_VALUE_LENGTH # flags + netid + iv + auth
201 if len(data) < min_length:
202 msg = f"Expected at least {min_length} bytes, got {len(data)}"
203 raise ValueError(msg)
205 flags = SecureNetworkBeaconFlags(data[0])
206 network_id = bytes(data[1:9])
207 iv_index = struct.unpack("<I", data[9:13])[0]
208 auth_value = bytes(data[13:21])
210 return cls(
211 network_id=network_id,
212 iv_index=iv_index,
213 key_refresh_flag=bool(flags & SecureNetworkBeaconFlags.KEY_REFRESH),
214 iv_update_flag=bool(flags & SecureNetworkBeaconFlags.IV_UPDATE),
215 authentication_value=auth_value,
216 )
219class UnprovisionedDeviceBeacon(msgspec.Struct, frozen=True, kw_only=True):
220 """Mesh Unprovisioned Device Beacon per Bluetooth Mesh Profile 3.10.2.
222 This beacon is broadcast by unprovisioned devices to announce
223 their presence and provisioning capabilities.
225 Attributes:
226 device_uuid: 16-byte device UUID
227 oob_info: OOB information flags
228 uri_hash: Optional 4-byte URI hash
230 """
232 device_uuid: bytes # 16 bytes
233 oob_info: int = 0x0000
234 uri_hash: bytes | None = None # 4 bytes, optional
236 def encode(self) -> bytearray:
237 """Encode to beacon format.
239 Returns:
240 Encoded beacon bytes
242 Raises:
243 ValueError: If device_uuid is not 16 bytes
245 """
246 if len(self.device_uuid) != DEVICE_UUID_LENGTH:
247 msg = f"device_uuid must be {DEVICE_UUID_LENGTH} bytes, got {len(self.device_uuid)}"
248 raise ValueError(msg)
250 result = bytearray(
251 struct.pack(
252 "<B16sH",
253 MeshBeaconType.UNPROVISIONED_DEVICE,
254 self.device_uuid,
255 self.oob_info,
256 )
257 )
259 if self.uri_hash is not None:
260 result.extend(self.uri_hash[:4])
262 return result
264 @classmethod
265 def decode(cls, data: bytes | bytearray) -> UnprovisionedDeviceBeacon:
266 """Decode from beacon bytes.
268 Args:
269 data: Raw beacon bytes (without beacon type byte)
271 Returns:
272 Parsed UnprovisionedDeviceBeacon
274 Raises:
275 ValueError: If data is too short
277 """
278 if len(data) < UNPROVISIONED_BEACON_MIN_LENGTH:
279 msg = f"Expected at least {UNPROVISIONED_BEACON_MIN_LENGTH} bytes, got {len(data)}"
280 raise ValueError(msg)
282 device_uuid = bytes(data[0:DEVICE_UUID_LENGTH])
283 oob_info = struct.unpack("<H", data[DEVICE_UUID_LENGTH:UNPROVISIONED_BEACON_MIN_LENGTH])[0]
284 uri_hash = (
285 bytes(data[UNPROVISIONED_BEACON_MIN_LENGTH:UNPROVISIONED_BEACON_WITH_HASH_LENGTH])
286 if len(data) >= UNPROVISIONED_BEACON_WITH_HASH_LENGTH
287 else None
288 )
290 return cls(
291 device_uuid=device_uuid,
292 oob_info=oob_info,
293 uri_hash=uri_hash,
294 )
297# MeshMessage constants
298MESH_MESSAGE_MIN_LENGTH = 9 # ivi/nid (1) + ctl/ttl (1) + seq (3) + src (2) + dst (2)
299MESH_MESSAGE_IVI_MASK = 0x80
300MESH_MESSAGE_NID_MASK = 0x7F
301MESH_MESSAGE_CTL_MASK = 0x80
302MESH_MESSAGE_TTL_MASK = 0x7F
305class MeshMessage(msgspec.Struct, frozen=True, kw_only=True):
306 """Mesh Network PDU message per Bluetooth Mesh Profile 3.4.
308 Attributes:
309 ivi: IV Index least significant bit
310 nid: Network ID (7 bits)
311 ctl: Control message flag
312 ttl: Time To Live
313 seq: Sequence number (24 bits)
314 src: Source address
315 dst: Destination address
316 transport_pdu: Encrypted transport PDU
317 net_mic: Network MIC (32 or 64 bits)
319 """
321 ivi: int = 0
322 nid: int = 0
323 ctl: bool = False
324 ttl: int = 0
325 seq: int = 0
326 src: int = 0
327 dst: int = 0
328 transport_pdu: bytes = b""
329 net_mic: bytes = b""
331 @classmethod
332 def decode(cls, data: bytes | bytearray) -> MeshMessage:
333 """Decode from Network PDU bytes.
335 Args:
336 data: Raw Network PDU bytes
338 Returns:
339 Parsed MeshMessage
341 Raises:
342 ValueError: If data is too short
344 """
345 if len(data) < MESH_MESSAGE_MIN_LENGTH:
346 msg = f"Expected at least {MESH_MESSAGE_MIN_LENGTH} bytes, got {len(data)}"
347 raise ValueError(msg)
349 # First byte: IVI (1 bit) + NID (7 bits)
350 ivi = (data[0] & MESH_MESSAGE_IVI_MASK) >> 7
351 nid = data[0] & MESH_MESSAGE_NID_MASK
353 # Second byte: CTL (1 bit) + TTL (7 bits)
354 ctl = bool(data[1] & MESH_MESSAGE_CTL_MASK)
355 ttl = data[1] & MESH_MESSAGE_TTL_MASK
357 # SEQ: 3 bytes big-endian
358 seq = (data[2] << 16) | (data[3] << 8) | data[4]
360 # SRC: 2 bytes big-endian
361 src = (data[5] << 8) | data[6]
363 # DST: 2 bytes big-endian
364 dst = (data[7] << 8) | data[8]
366 # Rest is transport PDU + NetMIC
367 # NetMIC length depends on message type per Bluetooth Mesh Profile 3.4.3
368 mic_length = CONTROL_MESSAGE_MIC_LENGTH if ctl else ACCESS_MESSAGE_MIC_LENGTH
369 transport_pdu = bytes(data[9:-mic_length]) if len(data) > MESH_MESSAGE_MIN_LENGTH + mic_length else b""
370 net_mic = bytes(data[-mic_length:]) if len(data) >= MESH_MESSAGE_MIN_LENGTH + mic_length else b""
372 return cls(
373 ivi=ivi,
374 nid=nid,
375 ctl=ctl,
376 ttl=ttl,
377 seq=seq,
378 src=src,
379 dst=dst,
380 transport_pdu=transport_pdu,
381 net_mic=net_mic,
382 )
385# ProvisioningBearerData constants
386PB_ADV_MIN_LENGTH = 6 # link_id (4) + transaction (1) + pdu_type (1)
389class ProvisioningBearerData(msgspec.Struct, frozen=True, kw_only=True):
390 """Provisioning Bearer (PB-ADV) data per Bluetooth Mesh Profile 5.3.
392 Attributes:
393 link_id: Link identifier (32 bits)
394 transaction_number: Transaction number
395 pdu_type: Provisioning PDU type
396 pdu_data: Raw PDU payload
398 """
400 link_id: int = 0
401 transaction_number: int = 0
402 pdu_type: ProvisioningPDUType = ProvisioningPDUType.INVITE
403 pdu_data: bytes = b""
405 @classmethod
406 def decode(cls, data: bytes | bytearray) -> ProvisioningBearerData:
407 """Decode from PB-ADV bytes.
409 Args:
410 data: Raw PB-ADV bytes
412 Returns:
413 Parsed ProvisioningBearerData
415 Raises:
416 ValueError: If data is too short
418 """
419 if len(data) < PB_ADV_MIN_LENGTH:
420 msg = f"Expected at least {PB_ADV_MIN_LENGTH} bytes, got {len(data)}"
421 raise ValueError(msg)
423 # Link ID: 4 bytes big-endian
424 link_id = struct.unpack(">I", data[0:4])[0]
426 # Transaction number: 1 byte
427 transaction_number = data[4]
429 # PDU type: 1 byte
430 try:
431 pdu_type = ProvisioningPDUType(data[5])
432 except ValueError:
433 pdu_type = ProvisioningPDUType.INVITE # Default for unknown types
435 # Rest is PDU data
436 pdu_data = bytes(data[6:]) if len(data) > PB_ADV_MIN_LENGTH else b""
438 return cls(
439 link_id=link_id,
440 transaction_number=transaction_number,
441 pdu_type=pdu_type,
442 pdu_data=pdu_data,
443 )
446__all__ = [
447 # Constants
448 "ACCESS_MESSAGE_MIC_LENGTH",
449 "ALGORITHM_FIPS_P256",
450 "AUTHENTICATION_VALUE_LENGTH",
451 "CONTROL_MESSAGE_MIC_LENGTH",
452 "DEVICE_UUID_LENGTH",
453 "MESH_MESSAGE_CTL_MASK",
454 "MESH_MESSAGE_IVI_MASK",
455 "MESH_MESSAGE_MIN_LENGTH",
456 "MESH_MESSAGE_NID_MASK",
457 "MESH_MESSAGE_TTL_MASK",
458 "NETWORK_ID_LENGTH",
459 "NETWORK_KEY_LENGTH",
460 "OOB_INFO_LENGTH",
461 "PB_ADV_MIN_LENGTH",
462 "PUBLIC_KEY_TYPE_NONE",
463 "STATIC_OOB_TYPE_NONE",
464 "UNPROVISIONED_BEACON_MIN_LENGTH",
465 "UNPROVISIONED_BEACON_WITH_HASH_LENGTH",
466 "URI_HASH_LENGTH",
467 # Enums
468 "MeshBeaconType",
469 "ProvisioningPDUType",
470 "SecureNetworkBeaconFlags",
471 # Structs
472 "MeshCapabilities",
473 "MeshMessage",
474 "ProvisioningBearerData",
475 "SecureNetworkBeacon",
476 "UnprovisionedDeviceBeacon",
477]