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

1"""Type definitions for BLE Encrypted Advertising Data (EAD). 

2 

3This module defines data structures for Encrypted Advertising Data 

4per Bluetooth Core Spec Supplement Section 1.23. 

5 

6EAD Format:: 

7 

8 [Randomizer (5 bytes)][Encrypted Payload (variable)][MIC (4 bytes)] 

9 

10The Randomizer provides uniqueness for each advertisement. The MIC 

11(Message Integrity Check) is a 4-byte authentication tag produced by 

12AES-CCM encryption. 

13 

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""" 

20 

21from __future__ import annotations 

22 

23from enum import Enum, auto 

24 

25import msgspec 

26from typing_extensions import Self 

27 

28 

29class EADError(Enum): 

30 """Error types for EAD decryption failures. 

31 

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 """ 

39 

40 INVALID_KEY = auto() 

41 REPLAY_DETECTED = auto() 

42 CORRUPTED_DATA = auto() 

43 NO_KEY_AVAILABLE = auto() 

44 INSUFFICIENT_DATA = auto() 

45 

46 

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 

51 

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 

57 

58 

59class EncryptedAdvertisingData(msgspec.Struct, frozen=True, kw_only=True): 

60 """Parsed BLE Encrypted Advertising Data structure. 

61 

62 Represents the three components of an EAD advertisement as defined 

63 in Bluetooth Core Spec Supplement Section 1.23. 

64 

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) 

69 

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 """ 

78 

79 randomizer: bytes 

80 encrypted_payload: bytes 

81 mic: bytes 

82 

83 @classmethod 

84 def from_bytes(cls, data: bytes) -> Self: 

85 """Parse raw EAD bytes into structured components. 

86 

87 Args: 

88 data: Raw EAD advertisement data (minimum 9 bytes: 

89 5-byte randomizer + at least 0-byte payload + 4-byte MIC) 

90 

91 Returns: 

92 Parsed EncryptedAdvertisingData structure 

93 

94 Raises: 

95 ValueError: If data is shorter than minimum EAD size (9 bytes) 

96 

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) 

106 

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 ) 

112 

113 

114class EADDecryptResult(msgspec.Struct, frozen=True, kw_only=True): 

115 """Result of an EAD decryption attempt. 

116 

117 Provides structured feedback on decryption success or failure, 

118 including specific error types for appropriate handling. 

119 

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 

125 

126 Example - successful decryption: 

127 >>> result = EADDecryptResult(success=True, plaintext=b"sensor_data") 

128 >>> if result.success: 

129 ... process_data(result.plaintext) 

130 

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 """ 

141 

142 success: bool 

143 plaintext: bytes | None = None 

144 error: str | None = None 

145 error_type: EADError | None = None 

146 

147 

148class EADKeyMaterial(msgspec.Struct, frozen=True, kw_only=True): 

149 """Key material for BLE Encrypted Advertising Data (EAD). 

150 

151 Per Bluetooth Core Spec Supplement Section 1.23, EAD encryption 

152 requires a 16-byte session key and 8-byte initialization vector. 

153 

154 Validation is performed at construction time - invalid key sizes 

155 will raise ValueError. 

156 

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) 

160 

161 Example: 

162 >>> key_material = EADKeyMaterial( 

163 ... session_key=bytes.fromhex("0123456789abcdef0123456789abcdef"), 

164 ... iv=bytes.fromhex("0102030405060708"), 

165 ... ) 

166 

167 Raises: 

168 ValueError: If session_key is not 16 bytes or iv is not 8 bytes 

169 """ 

170 

171 session_key: bytes 

172 iv: bytes 

173 

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)