Coverage for src / bluetooth_sig / types / ead.py: 100%
42 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""Type definitions for BLE Encrypted Advertising Data (EAD).
3This module defines data structures for Encrypted Advertising Data
4per Bluetooth Core Spec Supplement Section 1.23.
6EAD Format::
8 [Randomizer (5 bytes)][Encrypted Payload (variable)][MIC (4 bytes)]
10The Randomizer provides uniqueness for each advertisement. The MIC
11(Message Integrity Check) is a 4-byte authentication tag produced by
12AES-CCM encryption.
14Cryptographic parameters per Bluetooth Core Spec Supplement:
15 - Session Key: 16-byte AES-128 key
16 - IV: 8-byte initialization vector
17 - Nonce: 13 bytes (Randomizer + Device Address + Padding)
18 - MIC: 4-byte authentication tag
19"""
21from __future__ import annotations
23from enum import Enum, auto
25import msgspec
26from typing_extensions import Self
29class EADError(Enum):
30 """Error types for EAD decryption failures.
32 Attributes:
33 INVALID_KEY: Decryption failed due to incorrect key (MIC mismatch)
34 REPLAY_DETECTED: Advertisement counter indicates replay attack
35 CORRUPTED_DATA: Data format invalid or too short
36 NO_KEY_AVAILABLE: No encryption key configured for this device
37 INSUFFICIENT_DATA: EAD payload too short to contain required fields
38 """
40 INVALID_KEY = auto()
41 REPLAY_DETECTED = auto()
42 CORRUPTED_DATA = auto()
43 NO_KEY_AVAILABLE = auto()
44 INSUFFICIENT_DATA = auto()
47# EAD format constants per Bluetooth Core Spec Supplement Section 1.23
48EAD_RANDOMIZER_SIZE: int = 5 # 5-byte randomizer for nonce
49EAD_MIC_SIZE: int = 4 # 4-byte Message Integrity Check (tag)
50EAD_MIN_SIZE: int = EAD_RANDOMIZER_SIZE + EAD_MIC_SIZE # 9 bytes minimum
52# EAD cryptographic constants per Bluetooth Core Spec Supplement
53EAD_SESSION_KEY_SIZE: int = 16 # 128-bit AES session key
54EAD_IV_SIZE: int = 8 # 8-byte initialization vector
55EAD_NONCE_SIZE: int = 13 # Randomizer(5) + Address(6) + Padding(2)
56EAD_ADDRESS_SIZE: int = 6 # BLE device address size
59class EncryptedAdvertisingData(msgspec.Struct, frozen=True, kw_only=True):
60 """Parsed BLE Encrypted Advertising Data structure.
62 Represents the three components of an EAD advertisement as defined
63 in Bluetooth Core Spec Supplement Section 1.23.
65 Attributes:
66 randomizer: 5-byte randomizer for nonce construction
67 encrypted_payload: Variable-length encrypted data
68 mic: 4-byte Message Integrity Check (authentication tag)
70 Example:
71 >>> raw = bytes.fromhex("0102030405aabbccdd11223344")
72 >>> ead = EncryptedAdvertisingData.from_bytes(raw)
73 >>> print(ead.randomizer.hex())
74 '0102030405'
75 >>> print(ead.mic.hex())
76 '11223344'
77 """
79 randomizer: bytes
80 encrypted_payload: bytes
81 mic: bytes
83 @classmethod
84 def from_bytes(cls, data: bytes) -> Self:
85 """Parse raw EAD bytes into structured components.
87 Args:
88 data: Raw EAD advertisement data (minimum 9 bytes:
89 5-byte randomizer + at least 0-byte payload + 4-byte MIC)
91 Returns:
92 Parsed EncryptedAdvertisingData structure
94 Raises:
95 ValueError: If data is shorter than minimum EAD size (9 bytes)
97 Example:
98 >>> raw = bytes.fromhex("0102030405aabbccdd11223344")
99 >>> ead = EncryptedAdvertisingData.from_bytes(raw)
100 >>> len(ead.encrypted_payload)
101 4
102 """
103 if len(data) < EAD_MIN_SIZE:
104 msg = f"EAD data must be at least {EAD_MIN_SIZE} bytes (randomizer + MIC), got {len(data)} bytes"
105 raise ValueError(msg)
107 return cls(
108 randomizer=data[:EAD_RANDOMIZER_SIZE],
109 encrypted_payload=data[EAD_RANDOMIZER_SIZE:-EAD_MIC_SIZE],
110 mic=data[-EAD_MIC_SIZE:],
111 )
114class EADDecryptResult(msgspec.Struct, frozen=True, kw_only=True):
115 """Result of an EAD decryption attempt.
117 Provides structured feedback on decryption success or failure,
118 including specific error types for appropriate handling.
120 Attributes:
121 success: Whether decryption succeeded
122 plaintext: Decrypted data if successful, None otherwise
123 error: Human-readable error message if failed
124 error_type: Structured error type for programmatic handling
126 Example - successful decryption:
127 >>> result = EADDecryptResult(success=True, plaintext=b"sensor_data")
128 >>> if result.success:
129 ... process_data(result.plaintext)
131 Example - failed decryption:
132 >>> result = EADDecryptResult(
133 ... success=False,
134 ... plaintext=None,
135 ... error="MIC verification failed",
136 ... error_type=EADError.INVALID_KEY,
137 ... )
138 >>> if result.error_type == EADError.INVALID_KEY:
139 ... request_new_key()
140 """
142 success: bool
143 plaintext: bytes | None = None
144 error: str | None = None
145 error_type: EADError | None = None
148class EADKeyMaterial(msgspec.Struct, frozen=True, kw_only=True):
149 """Key material for BLE Encrypted Advertising Data (EAD).
151 Per Bluetooth Core Spec Supplement Section 1.23, EAD encryption
152 requires a 16-byte session key and 8-byte initialization vector.
154 Validation is performed at construction time - invalid key sizes
155 will raise ValueError.
157 Attributes:
158 session_key: 16-byte AES-128 session key for encryption/decryption
159 iv: 8-byte initialization vector (combined with randomizer for nonce)
161 Example:
162 >>> key_material = EADKeyMaterial(
163 ... session_key=bytes.fromhex("0123456789abcdef0123456789abcdef"),
164 ... iv=bytes.fromhex("0102030405060708"),
165 ... )
167 Raises:
168 ValueError: If session_key is not 16 bytes or iv is not 8 bytes
169 """
171 session_key: bytes
172 iv: bytes
174 def __post_init__(self) -> None:
175 """Validate key material sizes per Bluetooth Core Spec."""
176 if len(self.session_key) != EAD_SESSION_KEY_SIZE:
177 msg = f"EAD session key must be {EAD_SESSION_KEY_SIZE} bytes, got {len(self.session_key)}"
178 raise ValueError(msg)
179 if len(self.iv) != EAD_IV_SIZE:
180 msg = f"EAD IV must be {EAD_IV_SIZE} bytes, got {len(self.iv)}"
181 raise ValueError(msg)