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.
- bumble/_version.py +2 -2
- bumble/apps/bench.py +4 -2
- bumble/apps/console.py +2 -2
- bumble/apps/pair.py +185 -32
- bumble/att.py +13 -12
- bumble/avctp.py +2 -2
- bumble/avdtp.py +122 -68
- bumble/avrcp.py +11 -5
- bumble/core.py +13 -7
- bumble/{crypto.py → crypto/__init__.py} +11 -95
- bumble/crypto/builtin.py +652 -0
- bumble/crypto/cryptography.py +84 -0
- bumble/device.py +362 -185
- bumble/drivers/intel.py +3 -0
- bumble/gatt.py +3 -5
- bumble/gatt_client.py +5 -3
- bumble/gatt_server.py +8 -6
- bumble/hci.py +67 -2
- bumble/hfp.py +44 -20
- bumble/hid.py +24 -12
- bumble/host.py +24 -0
- bumble/keys.py +64 -48
- bumble/l2cap.py +19 -9
- bumble/pandora/host.py +11 -11
- bumble/pandora/l2cap.py +2 -2
- bumble/pandora/security.py +72 -56
- bumble/profiles/aics.py +3 -5
- bumble/profiles/ancs.py +3 -1
- bumble/profiles/ascs.py +11 -5
- bumble/profiles/asha.py +11 -6
- bumble/profiles/csip.py +1 -3
- bumble/profiles/gatt_service.py +1 -3
- bumble/profiles/hap.py +16 -33
- bumble/profiles/mcp.py +12 -9
- bumble/profiles/vcs.py +5 -5
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +17 -8
- bumble/smp.py +14 -8
- {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/METADATA +4 -4
- {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/RECORD +44 -42
- {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/WHEEL +1 -1
- {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.211.dist-info → bumble-0.0.212.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@
|
|
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
|
-
@
|
|
91
|
-
def from_dict(keys_dict):
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
@
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1107
|
+
self.emit(self.EVENT_OPEN)
|
|
1102
1108
|
elif new_state == self.State.DISCONNECTED:
|
|
1103
|
-
self.emit(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
624
|
+
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
|
625
625
|
)
|
|
626
626
|
result_handler = self.device.on( # type: ignore
|
|
627
|
-
|
|
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(
|
|
647
|
-
self.device.remove_listener(
|
|
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(
|
|
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(
|
|
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.')
|
bumble/pandora/security.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
517
|
+
return self._le_level_reached(level, connection)
|
|
502
518
|
|
|
503
|
-
return
|
|
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:
|
|
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:
|
|
322
|
+
def on_read(self, _connection: Connection) -> str:
|
|
324
323
|
return self.audio_input_description
|
|
325
324
|
|
|
326
|
-
async def on_write(self, connection:
|
|
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
|