Coverage for src / bluetooth_sig / advertising / exceptions.py: 100%

49 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Advertising exceptions for the Bluetooth SIG library. 

2 

3Provides exception types for advertising-related errors, following the same 

4patterns as GATT exceptions for API consistency. 

5 

6Exception Hierarchy: 

7 AdvertisingError (base) 

8 ├── AdvertisingParseError - General parse failures 

9 │ ├── EncryptionRequiredError - Payload encrypted, no bindkey 

10 │ ├── DecryptionFailedError - Decryption failed 

11 │ └── UnsupportedVersionError - Unknown protocol version 

12 ├── ReplayDetectedError - Counter not increasing 

13 └── DuplicatePacketError - Same packet_id as previous 

14""" 

15 

16from __future__ import annotations 

17 

18from bluetooth_sig.gatt.exceptions import BluetoothSIGError 

19 

20 

21class AdvertisingError(BluetoothSIGError): 

22 """Base exception for all advertising-related errors.""" 

23 

24 

25class AdvertisingParseError(AdvertisingError): 

26 """Exception raised when advertising payload parsing fails. 

27 

28 Attributes: 

29 message: Human-readable error message. 

30 raw_data: The raw advertising data that failed to parse. 

31 interpreter_name: Name of the interpreter that raised the error. 

32 field: Specific field that caused the error (if applicable). 

33 

34 """ 

35 

36 def __init__( 

37 self, 

38 message: str, 

39 raw_data: bytes = b"", 

40 interpreter_name: str = "", 

41 field: str | None = None, 

42 ) -> None: 

43 """Initialise AdvertisingParseError. 

44 

45 Args: 

46 message: Human-readable error message. 

47 raw_data: The raw advertising data that failed to parse. 

48 interpreter_name: Name of the interpreter that raised the error. 

49 field: Specific field that caused the error (if applicable). 

50 

51 """ 

52 self.raw_data = raw_data 

53 self.interpreter_name = interpreter_name 

54 self.field = field 

55 

56 # Build detailed message 

57 parts = [message] 

58 if interpreter_name: 

59 parts.insert(0, f"[{interpreter_name}]") 

60 if field: 

61 parts.append(f"(field: {field})") 

62 if raw_data: 

63 max_hex_bytes = 32 

64 hex_data = raw_data.hex() if len(raw_data) <= max_hex_bytes else f"{raw_data[:max_hex_bytes].hex()}..." 

65 parts.append(f"[data: {hex_data}]") 

66 

67 super().__init__(" ".join(parts)) 

68 

69 

70class EncryptionRequiredError(AdvertisingParseError): 

71 """Exception raised when payload is encrypted but no bindkey is available. 

72 

73 This exception indicates the payload contains encrypted data that 

74 requires a bindkey for decryption. The caller should: 

75 1. Prompt the user to provide a bindkey 

76 2. Store the bindkey in DeviceAdvertisingState.encryption.bindkey 

77 3. Retry interpretation 

78 

79 Attributes: 

80 mac_address: Device MAC address needing a bindkey. 

81 

82 """ 

83 

84 def __init__( 

85 self, 

86 mac_address: str, 

87 raw_data: bytes = b"", 

88 interpreter_name: str = "", 

89 ) -> None: 

90 """Initialise EncryptionRequiredError. 

91 

92 Args: 

93 mac_address: Device MAC address needing a bindkey. 

94 raw_data: The raw encrypted advertising data. 

95 interpreter_name: Name of the interpreter that raised the error. 

96 

97 """ 

98 self.mac_address = mac_address 

99 message = f"Encryption required for device {mac_address}" 

100 super().__init__( 

101 message=message, 

102 raw_data=raw_data, 

103 interpreter_name=interpreter_name, 

104 ) 

105 

106 

107class DecryptionFailedError(AdvertisingParseError): 

108 """Exception raised when decryption fails. 

109 

110 This typically indicates: 

111 - Wrong bindkey 

112 - Corrupted data 

113 - Incorrect nonce construction 

114 

115 Attributes: 

116 mac_address: Device MAC address. 

117 reason: Specific reason for decryption failure. 

118 

119 """ 

120 

121 def __init__( 

122 self, 

123 mac_address: str, 

124 reason: str = "decryption failed", 

125 raw_data: bytes = b"", 

126 interpreter_name: str = "", 

127 ) -> None: 

128 """Initialise DecryptionFailedError. 

129 

130 Args: 

131 mac_address: Device MAC address. 

132 reason: Specific reason for decryption failure. 

133 raw_data: The raw encrypted advertising data. 

134 interpreter_name: Name of the interpreter that raised the error. 

135 

136 """ 

137 self.mac_address = mac_address 

138 self.reason = reason 

139 message = f"Decryption failed for device {mac_address}: {reason}" 

140 super().__init__( 

141 message=message, 

142 raw_data=raw_data, 

143 interpreter_name=interpreter_name, 

144 ) 

145 

146 

147class UnsupportedVersionError(AdvertisingParseError): 

148 """Exception raised when protocol version is not supported. 

149 

150 Attributes: 

151 version: The unsupported version identifier. 

152 supported_versions: List of supported version identifiers. 

153 

154 """ 

155 

156 def __init__( 

157 self, 

158 version: str | int, 

159 supported_versions: list[str | int] | None = None, 

160 raw_data: bytes = b"", 

161 interpreter_name: str = "", 

162 ) -> None: 

163 """Initialise UnsupportedVersionError. 

164 

165 Args: 

166 version: The unsupported version identifier. 

167 supported_versions: List of supported version identifiers. 

168 raw_data: The raw advertising data. 

169 interpreter_name: Name of the interpreter that raised the error. 

170 

171 """ 

172 self.version = version 

173 self.supported_versions = supported_versions or [] 

174 supported_str = ", ".join(str(v) for v in self.supported_versions) if self.supported_versions else "unknown" 

175 message = f"Unsupported protocol version {version} (supported: {supported_str})" 

176 super().__init__( 

177 message=message, 

178 raw_data=raw_data, 

179 interpreter_name=interpreter_name, 

180 ) 

181 

182 

183class ReplayDetectedError(AdvertisingError): 

184 """Exception raised when a replay attack is detected. 

185 

186 This occurs when the encryption counter is not increasing, 

187 indicating a potential replay attack. 

188 

189 Note: Per Bluetooth Core Specification, replay protection is typically 

190 handled at Controller/Link Layer level. This exception is provided 

191 for vendor protocols that implement their own replay detection. 

192 

193 Attributes: 

194 mac_address: Device MAC address. 

195 received_counter: Counter value received in the packet. 

196 expected_counter: Minimum expected counter value. 

197 

198 """ 

199 

200 def __init__( 

201 self, 

202 mac_address: str, 

203 received_counter: int, 

204 expected_counter: int, 

205 ) -> None: 

206 """Initialise ReplayDetectedError. 

207 

208 Args: 

209 mac_address: Device MAC address. 

210 received_counter: Counter value received in the packet. 

211 expected_counter: Minimum expected counter value. 

212 

213 """ 

214 self.mac_address = mac_address 

215 self.received_counter = received_counter 

216 self.expected_counter = expected_counter 

217 message = ( 

218 f"Replay detected for device {mac_address}: " 

219 f"received counter {received_counter}, expected >= {expected_counter}" 

220 ) 

221 super().__init__(message) 

222 

223 

224class DuplicatePacketError(AdvertisingError): 

225 """Exception raised when a duplicate packet is detected. 

226 

227 This occurs when the same packet_id is received twice, indicating 

228 the same advertisement was received multiple times. This is typically 

229 not an error but may be useful for deduplication. 

230 

231 Attributes: 

232 mac_address: Device MAC address. 

233 packet_id: The duplicate packet ID. 

234 

235 """ 

236 

237 def __init__( 

238 self, 

239 mac_address: str, 

240 packet_id: int, 

241 ) -> None: 

242 """Initialise DuplicatePacketError. 

243 

244 Args: 

245 mac_address: Device MAC address. 

246 packet_id: The duplicate packet ID. 

247 

248 """ 

249 self.mac_address = mac_address 

250 self.packet_id = packet_id 

251 message = f"Duplicate packet {packet_id} from device {mac_address}" 

252 super().__init__(message)