bumble 0.0.211__py3-none-any.whl → 0.0.212__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +4 -2
  3. bumble/apps/console.py +2 -2
  4. bumble/apps/pair.py +185 -32
  5. bumble/att.py +13 -12
  6. bumble/avctp.py +2 -2
  7. bumble/avdtp.py +122 -68
  8. bumble/avrcp.py +11 -5
  9. bumble/core.py +13 -7
  10. bumble/{crypto.py → crypto/__init__.py} +11 -95
  11. bumble/crypto/builtin.py +652 -0
  12. bumble/crypto/cryptography.py +84 -0
  13. bumble/device.py +362 -185
  14. bumble/drivers/intel.py +3 -0
  15. bumble/gatt.py +3 -5
  16. bumble/gatt_client.py +5 -3
  17. bumble/gatt_server.py +8 -6
  18. bumble/hci.py +67 -2
  19. bumble/hfp.py +44 -20
  20. bumble/hid.py +24 -12
  21. bumble/host.py +24 -0
  22. bumble/keys.py +64 -48
  23. bumble/l2cap.py +19 -9
  24. bumble/pandora/host.py +11 -11
  25. bumble/pandora/l2cap.py +2 -2
  26. bumble/pandora/security.py +72 -56
  27. bumble/profiles/aics.py +3 -5
  28. bumble/profiles/ancs.py +3 -1
  29. bumble/profiles/ascs.py +11 -5
  30. bumble/profiles/asha.py +11 -6
  31. bumble/profiles/csip.py +1 -3
  32. bumble/profiles/gatt_service.py +1 -3
  33. bumble/profiles/hap.py +16 -33
  34. bumble/profiles/mcp.py +12 -9
  35. bumble/profiles/vcs.py +5 -5
  36. bumble/profiles/vocs.py +6 -9
  37. bumble/rfcomm.py +17 -8
  38. bumble/smp.py +14 -8
  39. {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/METADATA +4 -4
  40. {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/RECORD +44 -42
  41. {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/WHEEL +1 -1
  42. {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/entry_points.txt +0 -0
  43. {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/licenses/LICENSE +0 -0
  44. {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/top_level.txt +0 -0
bumble/keys.py CHANGED
@@ -22,14 +22,15 @@
22
22
  # -----------------------------------------------------------------------------
23
23
  from __future__ import annotations
24
24
  import asyncio
25
+ import dataclasses
25
26
  import logging
26
27
  import os
27
28
  import json
28
- from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
29
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
29
30
  from typing_extensions import Self
30
31
 
31
32
  from bumble.colors import color
32
- from bumble.hci import Address
33
+ from bumble import hci
33
34
 
34
35
  if TYPE_CHECKING:
35
36
  from bumble.device import Device
@@ -42,16 +43,17 @@ logger = logging.getLogger(__name__)
42
43
 
43
44
 
44
45
  # -----------------------------------------------------------------------------
46
+ @dataclasses.dataclass
45
47
  class PairingKeys:
48
+ @dataclasses.dataclass
46
49
  class Key:
47
- def __init__(self, value, authenticated=False, ediv=None, rand=None):
48
- self.value = value
49
- self.authenticated = authenticated
50
- self.ediv = ediv
51
- self.rand = rand
50
+ value: bytes
51
+ authenticated: bool = False
52
+ ediv: Optional[int] = None
53
+ rand: Optional[bytes] = None
52
54
 
53
55
  @classmethod
54
- def from_dict(cls, key_dict):
56
+ def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
55
57
  value = bytes.fromhex(key_dict['value'])
56
58
  authenticated = key_dict.get('authenticated', False)
57
59
  ediv = key_dict.get('ediv')
@@ -61,7 +63,7 @@ class PairingKeys:
61
63
 
62
64
  return cls(value, authenticated, ediv, rand)
63
65
 
64
- def to_dict(self):
66
+ def to_dict(self) -> dict[str, Any]:
65
67
  key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
66
68
  if self.ediv is not None:
67
69
  key_dict['ediv'] = self.ediv
@@ -70,39 +72,42 @@ class PairingKeys:
70
72
 
71
73
  return key_dict
72
74
 
73
- def __init__(self):
74
- self.address_type = None
75
- self.ltk = None
76
- self.ltk_central = None
77
- self.ltk_peripheral = None
78
- self.irk = None
79
- self.csrk = None
80
- self.link_key = None # Classic
81
-
82
- @staticmethod
83
- def key_from_dict(keys_dict, key_name):
75
+ address_type: Optional[hci.AddressType] = None
76
+ ltk: Optional[Key] = None
77
+ ltk_central: Optional[Key] = None
78
+ ltk_peripheral: Optional[Key] = None
79
+ irk: Optional[Key] = None
80
+ csrk: Optional[Key] = None
81
+ link_key: Optional[Key] = None # Classic
82
+ link_key_type: Optional[int] = None # Classic
83
+
84
+ @classmethod
85
+ def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
84
86
  key_dict = keys_dict.get(key_name)
85
87
  if key_dict is None:
86
88
  return None
87
89
 
88
90
  return PairingKeys.Key.from_dict(key_dict)
89
91
 
90
- @staticmethod
91
- def from_dict(keys_dict):
92
- keys = PairingKeys()
93
-
94
- keys.address_type = keys_dict.get('address_type')
95
- keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
96
- keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
97
- keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
98
- keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
99
- keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
100
- keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
101
-
102
- return keys
103
-
104
- def to_dict(self):
105
- keys = {}
92
+ @classmethod
93
+ def from_dict(cls, keys_dict: dict[str, Any]) -> PairingKeys:
94
+ return PairingKeys(
95
+ address_type=(
96
+ hci.AddressType(t)
97
+ if (t := keys_dict.get('address_type')) is not None
98
+ else None
99
+ ),
100
+ ltk=PairingKeys.key_from_dict(keys_dict, 'ltk'),
101
+ ltk_central=PairingKeys.key_from_dict(keys_dict, 'ltk_central'),
102
+ ltk_peripheral=PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral'),
103
+ irk=PairingKeys.key_from_dict(keys_dict, 'irk'),
104
+ csrk=PairingKeys.key_from_dict(keys_dict, 'csrk'),
105
+ link_key=PairingKeys.key_from_dict(keys_dict, 'link_key'),
106
+ link_key_type=keys_dict.get('link_key_type'),
107
+ )
108
+
109
+ def to_dict(self) -> dict[str, Any]:
110
+ keys: dict[str, Any] = {}
106
111
 
107
112
  if self.address_type is not None:
108
113
  keys['address_type'] = self.address_type
@@ -125,9 +130,12 @@ class PairingKeys:
125
130
  if self.link_key is not None:
126
131
  keys['link_key'] = self.link_key.to_dict()
127
132
 
133
+ if self.link_key_type is not None:
134
+ keys['link_key_type'] = self.link_key_type
135
+
128
136
  return keys
129
137
 
130
- def print(self, prefix=''):
138
+ def print(self, prefix: str = '') -> None:
131
139
  keys_dict = self.to_dict()
132
140
  for container_property, value in keys_dict.items():
133
141
  if isinstance(value, dict):
@@ -156,20 +164,28 @@ class KeyStore:
156
164
  all_keys = await self.get_all()
157
165
  await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
158
166
 
159
- async def get_resolving_keys(self):
167
+ async def get_resolving_keys(self) -> list[tuple[bytes, hci.Address]]:
160
168
  all_keys = await self.get_all()
161
169
  resolving_keys = []
162
170
  for name, keys in all_keys:
163
171
  if keys.irk is not None:
164
- if keys.address_type is None:
165
- address_type = Address.RANDOM_DEVICE_ADDRESS
166
- else:
167
- address_type = keys.address_type
168
- resolving_keys.append((keys.irk.value, Address(name, address_type)))
172
+ resolving_keys.append(
173
+ (
174
+ keys.irk.value,
175
+ hci.Address(
176
+ name,
177
+ (
178
+ keys.address_type
179
+ if keys.address_type is not None
180
+ else hci.Address.RANDOM_DEVICE_ADDRESS
181
+ ),
182
+ ),
183
+ )
184
+ )
169
185
 
170
186
  return resolving_keys
171
187
 
172
- async def print(self, prefix=''):
188
+ async def print(self, prefix: str = '') -> None:
173
189
  entries = await self.get_all()
174
190
  separator = ''
175
191
  for name, keys in entries:
@@ -177,8 +193,8 @@ class KeyStore:
177
193
  keys.print(prefix=prefix + ' ')
178
194
  separator = '\n'
179
195
 
180
- @staticmethod
181
- def create_for_device(device: Device) -> KeyStore:
196
+ @classmethod
197
+ def create_for_device(cls, device: Device) -> KeyStore:
182
198
  if device.config.keystore is None:
183
199
  return MemoryKeyStore()
184
200
 
@@ -266,9 +282,9 @@ class JsonKeyStore(KeyStore):
266
282
  filename = params[0]
267
283
 
268
284
  # Use a namespace based on the device address
269
- if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
285
+ if device.public_address not in (hci.Address.ANY, hci.Address.ANY_RANDOM):
270
286
  namespace = str(device.public_address)
271
- elif device.random_address != Address.ANY_RANDOM:
287
+ elif device.random_address != hci.Address.ANY_RANDOM:
272
288
  namespace = str(device.random_address)
273
289
  else:
274
290
  namespace = JsonKeyStore.DEFAULT_NAMESPACE
bumble/l2cap.py CHANGED
@@ -744,6 +744,9 @@ class ClassicChannel(utils.EventEmitter):
744
744
  WAIT_FINAL_RSP = 0x16
745
745
  WAIT_CONTROL_IND = 0x17
746
746
 
747
+ EVENT_OPEN = "open"
748
+ EVENT_CLOSE = "close"
749
+
747
750
  connection_result: Optional[asyncio.Future[None]]
748
751
  disconnection_result: Optional[asyncio.Future[None]]
749
752
  response: Optional[asyncio.Future[bytes]]
@@ -847,7 +850,7 @@ class ClassicChannel(utils.EventEmitter):
847
850
  def abort(self) -> None:
848
851
  if self.state == self.State.OPEN:
849
852
  self._change_state(self.State.CLOSED)
850
- self.emit('close')
853
+ self.emit(self.EVENT_CLOSE)
851
854
 
852
855
  def send_configure_request(self) -> None:
853
856
  options = L2CAP_Control_Frame.encode_configuration_options(
@@ -940,7 +943,7 @@ class ClassicChannel(utils.EventEmitter):
940
943
  if self.connection_result:
941
944
  self.connection_result.set_result(None)
942
945
  self.connection_result = None
943
- self.emit('open')
946
+ self.emit(self.EVENT_OPEN)
944
947
  elif self.state == self.State.WAIT_CONFIG_REQ_RSP:
945
948
  self._change_state(self.State.WAIT_CONFIG_RSP)
946
949
 
@@ -956,7 +959,7 @@ class ClassicChannel(utils.EventEmitter):
956
959
  if self.connection_result:
957
960
  self.connection_result.set_result(None)
958
961
  self.connection_result = None
959
- self.emit('open')
962
+ self.emit(self.EVENT_OPEN)
960
963
  else:
961
964
  logger.warning(color('invalid state', 'red'))
962
965
  elif (
@@ -991,7 +994,7 @@ class ClassicChannel(utils.EventEmitter):
991
994
  )
992
995
  )
993
996
  self._change_state(self.State.CLOSED)
994
- self.emit('close')
997
+ self.emit(self.EVENT_CLOSE)
995
998
  self.manager.on_channel_closed(self)
996
999
  else:
997
1000
  logger.warning(color('invalid state', 'red'))
@@ -1012,7 +1015,7 @@ class ClassicChannel(utils.EventEmitter):
1012
1015
  if self.disconnection_result:
1013
1016
  self.disconnection_result.set_result(None)
1014
1017
  self.disconnection_result = None
1015
- self.emit('close')
1018
+ self.emit(self.EVENT_CLOSE)
1016
1019
  self.manager.on_channel_closed(self)
1017
1020
 
1018
1021
  def __str__(self) -> str:
@@ -1047,6 +1050,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
1047
1050
  connection: Connection
1048
1051
  sink: Optional[Callable[[bytes], Any]]
1049
1052
 
1053
+ EVENT_OPEN = "open"
1054
+ EVENT_CLOSE = "close"
1055
+
1050
1056
  def __init__(
1051
1057
  self,
1052
1058
  manager: ChannelManager,
@@ -1098,9 +1104,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
1098
1104
  self.state = new_state
1099
1105
 
1100
1106
  if new_state == self.State.CONNECTED:
1101
- self.emit('open')
1107
+ self.emit(self.EVENT_OPEN)
1102
1108
  elif new_state == self.State.DISCONNECTED:
1103
- self.emit('close')
1109
+ self.emit(self.EVENT_CLOSE)
1104
1110
 
1105
1111
  def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
1106
1112
  self.manager.send_pdu(self.connection, self.destination_cid, pdu)
@@ -1381,6 +1387,8 @@ class LeCreditBasedChannel(utils.EventEmitter):
1381
1387
 
1382
1388
  # -----------------------------------------------------------------------------
1383
1389
  class ClassicChannelServer(utils.EventEmitter):
1390
+ EVENT_CONNECTION = "connection"
1391
+
1384
1392
  def __init__(
1385
1393
  self,
1386
1394
  manager: ChannelManager,
@@ -1395,7 +1403,7 @@ class ClassicChannelServer(utils.EventEmitter):
1395
1403
  self.mtu = mtu
1396
1404
 
1397
1405
  def on_connection(self, channel: ClassicChannel) -> None:
1398
- self.emit('connection', channel)
1406
+ self.emit(self.EVENT_CONNECTION, channel)
1399
1407
  if self.handler:
1400
1408
  self.handler(channel)
1401
1409
 
@@ -1406,6 +1414,8 @@ class ClassicChannelServer(utils.EventEmitter):
1406
1414
 
1407
1415
  # -----------------------------------------------------------------------------
1408
1416
  class LeCreditBasedChannelServer(utils.EventEmitter):
1417
+ EVENT_CONNECTION = "connection"
1418
+
1409
1419
  def __init__(
1410
1420
  self,
1411
1421
  manager: ChannelManager,
@@ -1424,7 +1434,7 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
1424
1434
  self.mps = mps
1425
1435
 
1426
1436
  def on_connection(self, channel: LeCreditBasedChannel) -> None:
1427
- self.emit('connection', channel)
1437
+ self.emit(self.EVENT_CONNECTION, channel)
1428
1438
  if self.handler:
1429
1439
  self.handler(channel)
1430
1440
 
bumble/pandora/host.py CHANGED
@@ -296,12 +296,12 @@ class HostService(HostServicer):
296
296
  def on_disconnection(_: None) -> None:
297
297
  disconnection_future.set_result(None)
298
298
 
299
- connection.on('disconnection', on_disconnection)
299
+ connection.on(connection.EVENT_DISCONNECTION, on_disconnection)
300
300
  try:
301
301
  await disconnection_future
302
302
  self.log.debug("Disconnected")
303
303
  finally:
304
- connection.remove_listener('disconnection', on_disconnection) # type: ignore
304
+ connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
305
305
 
306
306
  return empty_pb2.Empty()
307
307
 
@@ -383,7 +383,7 @@ class HostService(HostServicer):
383
383
  ):
384
384
  connections.put_nowait(connection)
385
385
 
386
- self.device.on('connection', on_connection)
386
+ self.device.on(self.device.EVENT_CONNECTION, on_connection)
387
387
 
388
388
  try:
389
389
  # Advertise until RPC is canceled
@@ -501,7 +501,7 @@ class HostService(HostServicer):
501
501
  ):
502
502
  connections.put_nowait(connection)
503
503
 
504
- self.device.on('connection', on_connection)
504
+ self.device.on(self.device.EVENT_CONNECTION, on_connection)
505
505
 
506
506
  try:
507
507
  while True:
@@ -531,7 +531,7 @@ class HostService(HostServicer):
531
531
  await asyncio.sleep(1)
532
532
  finally:
533
533
  if request.connectable:
534
- self.device.remove_listener('connection', on_connection) # type: ignore
534
+ self.device.remove_listener(self.device.EVENT_CONNECTION, on_connection) # type: ignore
535
535
 
536
536
  try:
537
537
  self.log.debug('Stop advertising')
@@ -557,7 +557,7 @@ class HostService(HostServicer):
557
557
  scanning_phys = [int(Phy.LE_1M), int(Phy.LE_CODED)]
558
558
 
559
559
  scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
560
- handler = self.device.on('advertisement', scan_queue.put_nowait)
560
+ handler = self.device.on(self.device.EVENT_ADVERTISEMENT, scan_queue.put_nowait)
561
561
  await self.device.start_scanning(
562
562
  legacy=request.legacy,
563
563
  active=not request.passive,
@@ -602,7 +602,7 @@ class HostService(HostServicer):
602
602
  yield sr
603
603
 
604
604
  finally:
605
- self.device.remove_listener('advertisement', handler) # type: ignore
605
+ self.device.remove_listener(self.device.EVENT_ADVERTISEMENT, handler) # type: ignore
606
606
  try:
607
607
  self.log.debug('Stop scanning')
608
608
  await bumble.utils.cancel_on_event(
@@ -621,10 +621,10 @@ class HostService(HostServicer):
621
621
  Optional[Tuple[Address, int, AdvertisingData, int]]
622
622
  ] = asyncio.Queue()
623
623
  complete_handler = self.device.on(
624
- 'inquiry_complete', lambda: inquiry_queue.put_nowait(None)
624
+ self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
625
625
  )
626
626
  result_handler = self.device.on( # type: ignore
627
- 'inquiry_result',
627
+ self.device.EVENT_INQUIRY_RESULT,
628
628
  lambda address, class_of_device, eir_data, rssi: inquiry_queue.put_nowait( # type: ignore
629
629
  (address, class_of_device, eir_data, rssi) # type: ignore
630
630
  ),
@@ -643,8 +643,8 @@ class HostService(HostServicer):
643
643
  )
644
644
 
645
645
  finally:
646
- self.device.remove_listener('inquiry_complete', complete_handler) # type: ignore
647
- self.device.remove_listener('inquiry_result', result_handler) # type: ignore
646
+ self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
647
+ self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
648
648
  try:
649
649
  self.log.debug('Stop inquiry')
650
650
  await bumble.utils.cancel_on_event(
bumble/pandora/l2cap.py CHANGED
@@ -83,7 +83,7 @@ class L2CAPService(L2CAPServicer):
83
83
  close_future.set_result(None)
84
84
 
85
85
  l2cap_channel.sink = on_channel_sdu
86
- l2cap_channel.on('close', on_close)
86
+ l2cap_channel.on(l2cap_channel.EVENT_CLOSE, on_close)
87
87
 
88
88
  return ChannelContext(close_future, sdu_queue)
89
89
 
@@ -151,7 +151,7 @@ class L2CAPService(L2CAPServicer):
151
151
  spec=spec, handler=on_l2cap_channel
152
152
  )
153
153
  else:
154
- l2cap_server.on('connection', on_l2cap_channel)
154
+ l2cap_server.on(l2cap_server.EVENT_CONNECTION, on_l2cap_channel)
155
155
 
156
156
  try:
157
157
  self.log.debug('Waiting for a channel connection.')
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
  import asyncio
17
17
  import contextlib
18
+ from collections.abc import Awaitable
18
19
  import grpc
19
20
  import logging
20
21
 
@@ -24,6 +25,7 @@ from bumble import hci
24
25
  from bumble.core import (
25
26
  PhysicalTransport,
26
27
  ProtocolError,
28
+ InvalidArgumentError,
27
29
  )
28
30
  import bumble.utils
29
31
  from bumble.device import Connection as BumbleConnection, Device
@@ -188,35 +190,6 @@ class PairingDelegate(BasePairingDelegate):
188
190
  self.service.event_queue.put_nowait(event)
189
191
 
190
192
 
191
- BR_LEVEL_REACHED: Dict[SecurityLevel, Callable[[BumbleConnection], bool]] = {
192
- LEVEL0: lambda connection: True,
193
- LEVEL1: lambda connection: connection.encryption == 0 or connection.authenticated,
194
- LEVEL2: lambda connection: connection.encryption != 0 and connection.authenticated,
195
- LEVEL3: lambda connection: connection.encryption != 0
196
- and connection.authenticated
197
- and connection.link_key_type
198
- in (
199
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
200
- hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
201
- ),
202
- LEVEL4: lambda connection: connection.encryption
203
- == hci.HCI_Encryption_Change_Event.AES_CCM
204
- and connection.authenticated
205
- and connection.link_key_type
206
- == hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
207
- }
208
-
209
- LE_LEVEL_REACHED: Dict[LESecurityLevel, Callable[[BumbleConnection], bool]] = {
210
- LE_LEVEL1: lambda connection: True,
211
- LE_LEVEL2: lambda connection: connection.encryption != 0,
212
- LE_LEVEL3: lambda connection: connection.encryption != 0
213
- and connection.authenticated,
214
- LE_LEVEL4: lambda connection: connection.encryption != 0
215
- and connection.authenticated
216
- and connection.sc,
217
- }
218
-
219
-
220
193
  class SecurityService(SecurityServicer):
221
194
  def __init__(self, device: Device, config: Config) -> None:
222
195
  self.log = utils.BumbleServerLoggerAdapter(
@@ -248,6 +221,59 @@ class SecurityService(SecurityServicer):
248
221
 
249
222
  self.device.pairing_config_factory = pairing_config_factory
250
223
 
224
+ async def _classic_level_reached(
225
+ self, level: SecurityLevel, connection: BumbleConnection
226
+ ) -> bool:
227
+ if level == LEVEL0:
228
+ return True
229
+ if level == LEVEL1:
230
+ return connection.encryption == 0 or connection.authenticated
231
+ if level == LEVEL2:
232
+ return connection.encryption != 0 and connection.authenticated
233
+
234
+ link_key_type: Optional[int] = None
235
+ if (keystore := connection.device.keystore) and (
236
+ keys := await keystore.get(str(connection.peer_address))
237
+ ):
238
+ link_key_type = keys.link_key_type
239
+ self.log.debug("link_key_type: %d", link_key_type)
240
+
241
+ if level == LEVEL3:
242
+ return (
243
+ connection.encryption != 0
244
+ and connection.authenticated
245
+ and link_key_type
246
+ in (
247
+ hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
248
+ hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
249
+ )
250
+ )
251
+ if level == LEVEL4:
252
+ return (
253
+ connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
254
+ and connection.authenticated
255
+ and link_key_type
256
+ == hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
257
+ )
258
+ raise InvalidArgumentError(f"Unexpected level {level}")
259
+
260
+ def _le_level_reached(
261
+ self, level: LESecurityLevel, connection: BumbleConnection
262
+ ) -> bool:
263
+ if level == LE_LEVEL1:
264
+ return True
265
+ if level == LE_LEVEL2:
266
+ return connection.encryption != 0
267
+ if level == LE_LEVEL3:
268
+ return connection.encryption != 0 and connection.authenticated
269
+ if level == LE_LEVEL4:
270
+ return (
271
+ connection.encryption != 0
272
+ and connection.authenticated
273
+ and connection.sc
274
+ )
275
+ raise InvalidArgumentError(f"Unexpected level {level}")
276
+
251
277
  @utils.rpc
252
278
  async def OnPairing(
253
279
  self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
@@ -290,7 +316,7 @@ class SecurityService(SecurityServicer):
290
316
  ] == oneof
291
317
 
292
318
  # security level already reached
293
- if self.reached_security_level(connection, level):
319
+ if await self.reached_security_level(connection, level):
294
320
  return SecureResponse(success=empty_pb2.Empty())
295
321
 
296
322
  # trigger pairing if needed
@@ -302,15 +328,15 @@ class SecurityService(SecurityServicer):
302
328
 
303
329
  with contextlib.closing(bumble.utils.EventWatcher()) as watcher:
304
330
 
305
- @watcher.on(connection, 'pairing')
331
+ @watcher.on(connection, connection.EVENT_PAIRING)
306
332
  def on_pairing(*_: Any) -> None:
307
333
  security_result.set_result('success')
308
334
 
309
- @watcher.on(connection, 'pairing_failure')
335
+ @watcher.on(connection, connection.EVENT_PAIRING_FAILURE)
310
336
  def on_pairing_failure(*_: Any) -> None:
311
337
  security_result.set_result('pairing_failure')
312
338
 
313
- @watcher.on(connection, 'disconnection')
339
+ @watcher.on(connection, connection.EVENT_DISCONNECTION)
314
340
  def on_disconnection(*_: Any) -> None:
315
341
  security_result.set_result('connection_died')
316
342
 
@@ -361,7 +387,7 @@ class SecurityService(SecurityServicer):
361
387
  return SecureResponse(encryption_failure=empty_pb2.Empty())
362
388
 
363
389
  # security level has been reached ?
364
- if self.reached_security_level(connection, level):
390
+ if await self.reached_security_level(connection, level):
365
391
  return SecureResponse(success=empty_pb2.Empty())
366
392
  return SecureResponse(not_reached=empty_pb2.Empty())
367
393
 
@@ -388,13 +414,10 @@ class SecurityService(SecurityServicer):
388
414
  pair_task: Optional[asyncio.Future[None]] = None
389
415
 
390
416
  async def authenticate() -> None:
391
- assert connection
392
417
  if (encryption := connection.encryption) != 0:
393
418
  self.log.debug('Disable encryption...')
394
- try:
419
+ with contextlib.suppress(Exception):
395
420
  await connection.encrypt(enable=False)
396
- except:
397
- pass
398
421
  self.log.debug('Disable encryption: done')
399
422
 
400
423
  self.log.debug('Authenticate...')
@@ -413,15 +436,13 @@ class SecurityService(SecurityServicer):
413
436
 
414
437
  return wrapper
415
438
 
416
- def try_set_success(*_: Any) -> None:
417
- assert connection
418
- if self.reached_security_level(connection, level):
439
+ async def try_set_success(*_: Any) -> None:
440
+ if await self.reached_security_level(connection, level):
419
441
  self.log.debug('Wait for security: done')
420
442
  wait_for_security.set_result('success')
421
443
 
422
- def on_encryption_change(*_: Any) -> None:
423
- assert connection
424
- if self.reached_security_level(connection, level):
444
+ async def on_encryption_change(*_: Any) -> None:
445
+ if await self.reached_security_level(connection, level):
425
446
  self.log.debug('Wait for security: done')
426
447
  wait_for_security.set_result('success')
427
448
  elif (
@@ -436,7 +457,7 @@ class SecurityService(SecurityServicer):
436
457
  if self.need_pairing(connection, level):
437
458
  pair_task = asyncio.create_task(connection.pair())
438
459
 
439
- listeners: Dict[str, Callable[..., None]] = {
460
+ listeners: Dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
440
461
  'disconnection': set_failure('connection_died'),
441
462
  'pairing_failure': set_failure('pairing_failure'),
442
463
  'connection_authentication_failure': set_failure('authentication_failure'),
@@ -455,7 +476,7 @@ class SecurityService(SecurityServicer):
455
476
  watcher.on(connection, event, listener)
456
477
 
457
478
  # security level already reached
458
- if self.reached_security_level(connection, level):
479
+ if await self.reached_security_level(connection, level):
459
480
  return WaitSecurityResponse(success=empty_pb2.Empty())
460
481
 
461
482
  self.log.debug('Wait for security...')
@@ -465,24 +486,20 @@ class SecurityService(SecurityServicer):
465
486
  # wait for `authenticate` to finish if any
466
487
  if authenticate_task is not None:
467
488
  self.log.debug('Wait for authentication...')
468
- try:
489
+ with contextlib.suppress(Exception):
469
490
  await authenticate_task # type: ignore
470
- except:
471
- pass
472
491
  self.log.debug('Authenticated')
473
492
 
474
493
  # wait for `pair` to finish if any
475
494
  if pair_task is not None:
476
495
  self.log.debug('Wait for authentication...')
477
- try:
496
+ with contextlib.suppress(Exception):
478
497
  await pair_task # type: ignore
479
- except:
480
- pass
481
498
  self.log.debug('paired')
482
499
 
483
500
  return WaitSecurityResponse(**kwargs)
484
501
 
485
- def reached_security_level(
502
+ async def reached_security_level(
486
503
  self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
487
504
  ) -> bool:
488
505
  self.log.debug(
@@ -492,15 +509,14 @@ class SecurityService(SecurityServicer):
492
509
  'encryption': connection.encryption,
493
510
  'authenticated': connection.authenticated,
494
511
  'sc': connection.sc,
495
- 'link_key_type': connection.link_key_type,
496
512
  }
497
513
  )
498
514
  )
499
515
 
500
516
  if isinstance(level, LESecurityLevel):
501
- return LE_LEVEL_REACHED[level](connection)
517
+ return self._le_level_reached(level, connection)
502
518
 
503
- return BR_LEVEL_REACHED[level](connection)
519
+ return await self._classic_level_reached(level, connection)
504
520
 
505
521
  def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
506
522
  if connection.transport == PhysicalTransport.LE:
bumble/profiles/aics.py CHANGED
@@ -198,8 +198,7 @@ class AudioInputControlPoint:
198
198
  audio_input_state: AudioInputState
199
199
  gain_settings_properties: GainSettingsProperties
200
200
 
201
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
202
- assert connection
201
+ async def on_write(self, connection: Connection, value: bytes) -> None:
203
202
 
204
203
  opcode = AudioInputControlPointOpCode(value[0])
205
204
 
@@ -320,11 +319,10 @@ class AudioInputDescription:
320
319
  audio_input_description: str = "Bluetooth"
321
320
  attribute: Optional[Attribute] = None
322
321
 
323
- def on_read(self, _connection: Optional[Connection]) -> str:
322
+ def on_read(self, _connection: Connection) -> str:
324
323
  return self.audio_input_description
325
324
 
326
- async def on_write(self, connection: Optional[Connection], value: str) -> None:
327
- assert connection
325
+ async def on_write(self, connection: Connection, value: str) -> None:
328
326
  assert self.attribute
329
327
 
330
328
  self.audio_input_description = value