bumble 0.0.211__py3-none-any.whl → 0.0.213__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/a2dp.py +6 -0
- bumble/apps/README.md +0 -3
- bumble/apps/auracast.py +11 -9
- bumble/apps/bench.py +482 -31
- bumble/apps/console.py +5 -5
- bumble/apps/controller_info.py +47 -10
- bumble/apps/controller_loopback.py +7 -3
- bumble/apps/controllers.py +2 -2
- bumble/apps/device_info.py +2 -2
- bumble/apps/gatt_dump.py +2 -2
- bumble/apps/gg_bridge.py +2 -2
- bumble/apps/hci_bridge.py +2 -2
- bumble/apps/l2cap_bridge.py +2 -2
- bumble/apps/lea_unicast/app.py +6 -1
- bumble/apps/pair.py +204 -43
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/rfcomm_bridge.py +1 -1
- bumble/apps/scan.py +2 -2
- bumble/apps/show.py +4 -2
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +126 -18
- bumble/at.py +4 -4
- bumble/att.py +15 -18
- bumble/avc.py +7 -7
- bumble/avctp.py +5 -5
- bumble/avdtp.py +138 -88
- bumble/avrcp.py +52 -58
- bumble/colors.py +2 -2
- bumble/controller.py +84 -23
- 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 +688 -345
- bumble/drivers/__init__.py +2 -2
- bumble/drivers/common.py +0 -2
- bumble/drivers/intel.py +40 -40
- bumble/drivers/rtk.py +28 -35
- bumble/gatt.py +7 -9
- bumble/gatt_adapters.py +4 -5
- bumble/gatt_client.py +31 -34
- bumble/gatt_server.py +15 -17
- bumble/hci.py +2635 -2878
- bumble/helpers.py +4 -5
- bumble/hfp.py +76 -57
- bumble/hid.py +24 -12
- bumble/host.py +117 -34
- bumble/keys.py +68 -52
- bumble/l2cap.py +329 -403
- bumble/link.py +6 -270
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +1 -1
- bumble/pandora/config.py +2 -2
- bumble/pandora/device.py +6 -6
- bumble/pandora/host.py +38 -39
- bumble/pandora/l2cap.py +4 -4
- bumble/pandora/security.py +73 -57
- bumble/pandora/utils.py +3 -3
- bumble/profiles/aics.py +3 -5
- bumble/profiles/ancs.py +3 -1
- bumble/profiles/ascs.py +143 -136
- bumble/profiles/asha.py +13 -8
- bumble/profiles/bap.py +3 -4
- bumble/profiles/csip.py +3 -5
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +2 -2
- bumble/profiles/gatt_service.py +1 -3
- bumble/profiles/hap.py +42 -58
- bumble/profiles/le_audio.py +4 -4
- bumble/profiles/mcp.py +16 -13
- bumble/profiles/vcs.py +8 -10
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +27 -18
- bumble/rtp.py +1 -2
- bumble/sdp.py +2 -2
- bumble/smp.py +71 -69
- bumble/tools/rtk_util.py +2 -2
- bumble/transport/__init__.py +2 -16
- bumble/transport/android_netsim.py +5 -5
- bumble/transport/common.py +4 -4
- bumble/transport/pyusb.py +2 -2
- bumble/utils.py +2 -5
- bumble/vendor/android/hci.py +118 -200
- bumble/vendor/zephyr/hci.py +32 -27
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/METADATA +5 -5
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/RECORD +92 -93
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
- bumble/apps/link_relay/__init__.py +0 -0
- bumble/apps/link_relay/link_relay.py +0 -289
- bumble/apps/link_relay/logging.yml +0 -21
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/host.py
CHANGED
|
@@ -26,10 +26,7 @@ from typing import (
|
|
|
26
26
|
Any,
|
|
27
27
|
Awaitable,
|
|
28
28
|
Callable,
|
|
29
|
-
Deque,
|
|
30
|
-
Dict,
|
|
31
29
|
Optional,
|
|
32
|
-
Set,
|
|
33
30
|
cast,
|
|
34
31
|
TYPE_CHECKING,
|
|
35
32
|
)
|
|
@@ -41,7 +38,6 @@ from bumble.snoop import Snooper
|
|
|
41
38
|
from bumble import drivers
|
|
42
39
|
from bumble import hci
|
|
43
40
|
from bumble.core import (
|
|
44
|
-
PhysicalTransport,
|
|
45
41
|
PhysicalTransport,
|
|
46
42
|
ConnectionPHY,
|
|
47
43
|
ConnectionParameters,
|
|
@@ -75,6 +71,11 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
75
71
|
|
|
76
72
|
max_packet_size: int
|
|
77
73
|
|
|
74
|
+
class PerConnectionState:
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self.in_flight = 0
|
|
77
|
+
self.drained = asyncio.Event()
|
|
78
|
+
|
|
78
79
|
def __init__(
|
|
79
80
|
self,
|
|
80
81
|
max_packet_size: int,
|
|
@@ -85,11 +86,16 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
85
86
|
self.max_packet_size = max_packet_size
|
|
86
87
|
self.max_in_flight = max_in_flight
|
|
87
88
|
self._in_flight = 0 # Total number of packets in flight across all connections
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
)
|
|
89
|
+
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
|
|
90
|
+
collections.defaultdict(DataPacketQueue.PerConnectionState)
|
|
91
|
+
)
|
|
92
|
+
self._drained_per_connection: dict[int, asyncio.Event] = (
|
|
93
|
+
collections.defaultdict(asyncio.Event)
|
|
94
|
+
)
|
|
91
95
|
self._send = send
|
|
92
|
-
self._packets:
|
|
96
|
+
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
|
97
|
+
collections.deque()
|
|
98
|
+
)
|
|
93
99
|
self._queued = 0
|
|
94
100
|
self._completed = 0
|
|
95
101
|
|
|
@@ -137,36 +143,40 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
137
143
|
self._completed += flushed_count
|
|
138
144
|
self._packets = collections.deque(packets_to_keep)
|
|
139
145
|
|
|
140
|
-
if
|
|
141
|
-
in_flight =
|
|
146
|
+
if connection_state := self._connection_state.pop(connection_handle, None):
|
|
147
|
+
in_flight = connection_state.in_flight
|
|
142
148
|
self._completed += in_flight
|
|
143
149
|
self._in_flight -= in_flight
|
|
144
|
-
|
|
150
|
+
connection_state.drained.set()
|
|
145
151
|
|
|
146
152
|
def _check_queue(self) -> None:
|
|
147
153
|
while self._packets and self._in_flight < self.max_in_flight:
|
|
148
154
|
packet, connection_handle = self._packets.pop()
|
|
149
155
|
self._send(packet)
|
|
150
156
|
self._in_flight += 1
|
|
151
|
-
self.
|
|
157
|
+
connection_state = self._connection_state[connection_handle]
|
|
158
|
+
connection_state.in_flight += 1
|
|
159
|
+
connection_state.drained.clear()
|
|
152
160
|
|
|
153
161
|
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
|
154
162
|
"""Mark one or more packets associated with a connection as completed."""
|
|
155
|
-
if connection_handle not in self.
|
|
163
|
+
if connection_handle not in self._connection_state:
|
|
156
164
|
logger.warning(
|
|
157
165
|
f'received completion for unknown connection {connection_handle}'
|
|
158
166
|
)
|
|
159
167
|
return
|
|
160
168
|
|
|
161
|
-
|
|
162
|
-
if packet_count <=
|
|
163
|
-
|
|
169
|
+
connection_state = self._connection_state[connection_handle]
|
|
170
|
+
if packet_count <= connection_state.in_flight:
|
|
171
|
+
connection_state.in_flight -= packet_count
|
|
164
172
|
else:
|
|
165
173
|
logger.warning(
|
|
166
174
|
f'{packet_count} completed for {connection_handle} '
|
|
167
|
-
f'but only {
|
|
175
|
+
f'but only {connection_state.in_flight} in flight'
|
|
168
176
|
)
|
|
169
|
-
|
|
177
|
+
connection_state.in_flight = 0
|
|
178
|
+
if connection_state.in_flight == 0:
|
|
179
|
+
connection_state.drained.set()
|
|
170
180
|
|
|
171
181
|
if packet_count <= self._in_flight:
|
|
172
182
|
self._in_flight -= packet_count
|
|
@@ -181,6 +191,13 @@ class DataPacketQueue(utils.EventEmitter):
|
|
|
181
191
|
self._check_queue()
|
|
182
192
|
self.emit('flow')
|
|
183
193
|
|
|
194
|
+
async def drain(self, connection_handle: int) -> None:
|
|
195
|
+
"""Wait until there are no pending packets for a connection."""
|
|
196
|
+
if not (connection_state := self._connection_state.get(connection_handle)):
|
|
197
|
+
raise ValueError('no such connection')
|
|
198
|
+
|
|
199
|
+
await connection_state.drained.wait()
|
|
200
|
+
|
|
184
201
|
|
|
185
202
|
# -----------------------------------------------------------------------------
|
|
186
203
|
class Connection:
|
|
@@ -234,16 +251,16 @@ class IsoLink:
|
|
|
234
251
|
|
|
235
252
|
# -----------------------------------------------------------------------------
|
|
236
253
|
class Host(utils.EventEmitter):
|
|
237
|
-
connections:
|
|
238
|
-
cis_links:
|
|
239
|
-
bis_links:
|
|
240
|
-
sco_links:
|
|
254
|
+
connections: dict[int, Connection]
|
|
255
|
+
cis_links: dict[int, IsoLink]
|
|
256
|
+
bis_links: dict[int, IsoLink]
|
|
257
|
+
sco_links: dict[int, ScoLink]
|
|
241
258
|
bigs: dict[int, set[int]]
|
|
242
259
|
acl_packet_queue: Optional[DataPacketQueue] = None
|
|
243
260
|
le_acl_packet_queue: Optional[DataPacketQueue] = None
|
|
244
261
|
iso_packet_queue: Optional[DataPacketQueue] = None
|
|
245
262
|
hci_sink: Optional[TransportSink] = None
|
|
246
|
-
hci_metadata:
|
|
263
|
+
hci_metadata: dict[str, Any]
|
|
247
264
|
long_term_key_provider: Optional[
|
|
248
265
|
Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
|
|
249
266
|
]
|
|
@@ -435,6 +452,14 @@ class Host(utils.EventEmitter):
|
|
|
435
452
|
)
|
|
436
453
|
)
|
|
437
454
|
)
|
|
455
|
+
if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
|
|
456
|
+
await self.send_command(
|
|
457
|
+
hci.HCI_Set_Event_Mask_Page_2_Command(
|
|
458
|
+
event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
|
|
459
|
+
[hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
)
|
|
438
463
|
|
|
439
464
|
if (
|
|
440
465
|
self.local_version is not None
|
|
@@ -456,6 +481,7 @@ class Host(utils.EventEmitter):
|
|
|
456
481
|
hci.HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT,
|
|
457
482
|
hci.HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT,
|
|
458
483
|
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT,
|
|
484
|
+
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
|
459
485
|
hci.HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT,
|
|
460
486
|
hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
|
|
461
487
|
hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
|
|
@@ -804,7 +830,7 @@ class Host(utils.EventEmitter):
|
|
|
804
830
|
) != 0
|
|
805
831
|
|
|
806
832
|
@property
|
|
807
|
-
def supported_commands(self) ->
|
|
833
|
+
def supported_commands(self) -> set[int]:
|
|
808
834
|
return set(
|
|
809
835
|
op_code
|
|
810
836
|
for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
|
|
@@ -827,8 +853,8 @@ class Host(utils.EventEmitter):
|
|
|
827
853
|
def on_packet(self, packet: bytes) -> None:
|
|
828
854
|
try:
|
|
829
855
|
hci_packet = hci.HCI_Packet.from_bytes(packet)
|
|
830
|
-
except Exception
|
|
831
|
-
logger.
|
|
856
|
+
except Exception:
|
|
857
|
+
logger.exception('!!! error parsing packet from bytes')
|
|
832
858
|
return
|
|
833
859
|
|
|
834
860
|
if self.ready or (
|
|
@@ -1118,11 +1144,19 @@ class Host(utils.EventEmitter):
|
|
|
1118
1144
|
else:
|
|
1119
1145
|
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
|
1120
1146
|
|
|
1121
|
-
def on_hci_le_advertising_report_event(
|
|
1147
|
+
def on_hci_le_advertising_report_event(
|
|
1148
|
+
self,
|
|
1149
|
+
event: (
|
|
1150
|
+
hci.HCI_LE_Advertising_Report_Event
|
|
1151
|
+
| hci.HCI_LE_Extended_Advertising_Report_Event
|
|
1152
|
+
),
|
|
1153
|
+
):
|
|
1122
1154
|
for report in event.reports:
|
|
1123
1155
|
self.emit('advertising_report', report)
|
|
1124
1156
|
|
|
1125
|
-
def on_hci_le_extended_advertising_report_event(
|
|
1157
|
+
def on_hci_le_extended_advertising_report_event(
|
|
1158
|
+
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
|
|
1159
|
+
):
|
|
1126
1160
|
self.on_hci_le_advertising_report_event(event)
|
|
1127
1161
|
|
|
1128
1162
|
def on_hci_le_advertising_set_terminated_event(self, event):
|
|
@@ -1253,7 +1287,24 @@ class Host(utils.EventEmitter):
|
|
|
1253
1287
|
self.cis_links[event.connection_handle] = IsoLink(
|
|
1254
1288
|
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
|
1255
1289
|
)
|
|
1256
|
-
self.emit(
|
|
1290
|
+
self.emit(
|
|
1291
|
+
'cis_establishment',
|
|
1292
|
+
event.connection_handle,
|
|
1293
|
+
event.cig_sync_delay,
|
|
1294
|
+
event.cis_sync_delay,
|
|
1295
|
+
event.transport_latency_c_to_p,
|
|
1296
|
+
event.transport_latency_p_to_c,
|
|
1297
|
+
event.phy_c_to_p,
|
|
1298
|
+
event.phy_p_to_c,
|
|
1299
|
+
event.nse,
|
|
1300
|
+
event.bn_c_to_p,
|
|
1301
|
+
event.bn_p_to_c,
|
|
1302
|
+
event.ft_c_to_p,
|
|
1303
|
+
event.ft_p_to_c,
|
|
1304
|
+
event.max_pdu_c_to_p,
|
|
1305
|
+
event.max_pdu_p_to_c,
|
|
1306
|
+
event.iso_interval,
|
|
1307
|
+
)
|
|
1257
1308
|
else:
|
|
1258
1309
|
self.emit(
|
|
1259
1310
|
'cis_establishment_failure', event.connection_handle, event.status
|
|
@@ -1341,6 +1392,15 @@ class Host(utils.EventEmitter):
|
|
|
1341
1392
|
def on_hci_synchronous_connection_changed_event(self, event):
|
|
1342
1393
|
pass
|
|
1343
1394
|
|
|
1395
|
+
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
|
1396
|
+
self.emit(
|
|
1397
|
+
'mode_change',
|
|
1398
|
+
event.connection_handle,
|
|
1399
|
+
event.status,
|
|
1400
|
+
event.current_mode,
|
|
1401
|
+
event.interval,
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1344
1404
|
def on_hci_role_change_event(self, event):
|
|
1345
1405
|
if event.status == hci.HCI_SUCCESS:
|
|
1346
1406
|
logger.debug(
|
|
@@ -1356,6 +1416,10 @@ class Host(utils.EventEmitter):
|
|
|
1356
1416
|
self.emit('role_change_failure', event.bd_addr, event.status)
|
|
1357
1417
|
|
|
1358
1418
|
def on_hci_le_data_length_change_event(self, event):
|
|
1419
|
+
if (connection := self.connections.get(event.connection_handle)) is None:
|
|
1420
|
+
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
|
1421
|
+
return
|
|
1422
|
+
|
|
1359
1423
|
self.emit(
|
|
1360
1424
|
'connection_data_length_change',
|
|
1361
1425
|
event.connection_handle,
|
|
@@ -1376,13 +1440,30 @@ class Host(utils.EventEmitter):
|
|
|
1376
1440
|
event.status,
|
|
1377
1441
|
)
|
|
1378
1442
|
|
|
1379
|
-
def on_hci_encryption_change_event(self, event):
|
|
1443
|
+
def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
|
|
1380
1444
|
# Notify the client
|
|
1381
1445
|
if event.status == hci.HCI_SUCCESS:
|
|
1382
1446
|
self.emit(
|
|
1383
1447
|
'connection_encryption_change',
|
|
1384
1448
|
event.connection_handle,
|
|
1385
1449
|
event.encryption_enabled,
|
|
1450
|
+
0,
|
|
1451
|
+
)
|
|
1452
|
+
else:
|
|
1453
|
+
self.emit(
|
|
1454
|
+
'connection_encryption_failure', event.connection_handle, event.status
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
def on_hci_encryption_change_v2_event(
|
|
1458
|
+
self, event: hci.HCI_Encryption_Change_V2_Event
|
|
1459
|
+
):
|
|
1460
|
+
# Notify the client
|
|
1461
|
+
if event.status == hci.HCI_SUCCESS:
|
|
1462
|
+
self.emit(
|
|
1463
|
+
'connection_encryption_change',
|
|
1464
|
+
event.connection_handle,
|
|
1465
|
+
event.encryption_enabled,
|
|
1466
|
+
event.encryption_key_size,
|
|
1386
1467
|
)
|
|
1387
1468
|
else:
|
|
1388
1469
|
self.emit(
|
|
@@ -1496,13 +1577,15 @@ class Host(utils.EventEmitter):
|
|
|
1496
1577
|
self.emit('inquiry_complete')
|
|
1497
1578
|
|
|
1498
1579
|
def on_hci_inquiry_result_with_rssi_event(self, event):
|
|
1499
|
-
for
|
|
1580
|
+
for bd_addr, class_of_device, rssi in zip(
|
|
1581
|
+
event.bd_addr, event.class_of_device, event.rssi
|
|
1582
|
+
):
|
|
1500
1583
|
self.emit(
|
|
1501
1584
|
'inquiry_result',
|
|
1502
|
-
|
|
1503
|
-
|
|
1585
|
+
bd_addr,
|
|
1586
|
+
class_of_device,
|
|
1504
1587
|
b'',
|
|
1505
|
-
|
|
1588
|
+
rssi,
|
|
1506
1589
|
)
|
|
1507
1590
|
|
|
1508
1591
|
def on_hci_extended_inquiry_result_event(self, event):
|
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,
|
|
29
|
+
from typing import TYPE_CHECKING, Optional, 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):
|
|
@@ -149,27 +157,35 @@ class KeyStore:
|
|
|
149
157
|
async def get(self, _name: str) -> Optional[PairingKeys]:
|
|
150
158
|
return None
|
|
151
159
|
|
|
152
|
-
async def get_all(self) ->
|
|
160
|
+
async def get_all(self) -> list[tuple[str, PairingKeys]]:
|
|
153
161
|
return []
|
|
154
162
|
|
|
155
163
|
async def delete_all(self) -> None:
|
|
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
|
|
|
@@ -256,7 +272,7 @@ class JsonKeyStore(KeyStore):
|
|
|
256
272
|
|
|
257
273
|
@classmethod
|
|
258
274
|
def from_device(
|
|
259
|
-
cls:
|
|
275
|
+
cls: type[Self], device: Device, filename: Optional[str] = None
|
|
260
276
|
) -> Self:
|
|
261
277
|
if not filename:
|
|
262
278
|
# Extract the filename from the config if there is one
|
|
@@ -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
|
|
@@ -340,7 +356,7 @@ class JsonKeyStore(KeyStore):
|
|
|
340
356
|
|
|
341
357
|
# -----------------------------------------------------------------------------
|
|
342
358
|
class MemoryKeyStore(KeyStore):
|
|
343
|
-
all_keys:
|
|
359
|
+
all_keys: dict[str, PairingKeys]
|
|
344
360
|
|
|
345
361
|
def __init__(self) -> None:
|
|
346
362
|
self.all_keys = {}
|
|
@@ -355,5 +371,5 @@ class MemoryKeyStore(KeyStore):
|
|
|
355
371
|
async def get(self, name: str) -> Optional[PairingKeys]:
|
|
356
372
|
return self.all_keys.get(name)
|
|
357
373
|
|
|
358
|
-
async def get_all(self) ->
|
|
374
|
+
async def get_all(self) -> list[tuple[str, PairingKeys]]:
|
|
359
375
|
return list(self.all_keys.items())
|