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/pandora/host.py
CHANGED
|
@@ -73,7 +73,6 @@ from pandora.host_pb2 import (
|
|
|
73
73
|
ConnectResponse,
|
|
74
74
|
DataTypes,
|
|
75
75
|
DisconnectRequest,
|
|
76
|
-
DiscoverabilityMode,
|
|
77
76
|
InquiryResponse,
|
|
78
77
|
PrimaryPhy,
|
|
79
78
|
ReadLocalAddressResponse,
|
|
@@ -86,9 +85,9 @@ from pandora.host_pb2 import (
|
|
|
86
85
|
WaitConnectionResponse,
|
|
87
86
|
WaitDisconnectionRequest,
|
|
88
87
|
)
|
|
89
|
-
from typing import AsyncGenerator,
|
|
88
|
+
from typing import AsyncGenerator, Optional, cast
|
|
90
89
|
|
|
91
|
-
PRIMARY_PHY_MAP:
|
|
90
|
+
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
|
92
91
|
# Default value reported by Bumble for legacy Advertising reports.
|
|
93
92
|
# FIXME(uael): `None` might be a better value, but Bumble need to change accordingly.
|
|
94
93
|
0: PRIMARY_1M,
|
|
@@ -96,26 +95,26 @@ PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
|
|
|
96
95
|
3: PRIMARY_CODED,
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
SECONDARY_PHY_MAP:
|
|
98
|
+
SECONDARY_PHY_MAP: dict[int, SecondaryPhy] = {
|
|
100
99
|
0: SECONDARY_NONE,
|
|
101
100
|
1: SECONDARY_1M,
|
|
102
101
|
2: SECONDARY_2M,
|
|
103
102
|
3: SECONDARY_CODED,
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
PRIMARY_PHY_TO_BUMBLE_PHY_MAP:
|
|
105
|
+
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: dict[PrimaryPhy, Phy] = {
|
|
107
106
|
PRIMARY_1M: Phy.LE_1M,
|
|
108
107
|
PRIMARY_CODED: Phy.LE_CODED,
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
SECONDARY_PHY_TO_BUMBLE_PHY_MAP:
|
|
110
|
+
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: dict[SecondaryPhy, Phy] = {
|
|
112
111
|
SECONDARY_NONE: Phy.LE_1M,
|
|
113
112
|
SECONDARY_1M: Phy.LE_1M,
|
|
114
113
|
SECONDARY_2M: Phy.LE_2M,
|
|
115
114
|
SECONDARY_CODED: Phy.LE_CODED,
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
OWN_ADDRESS_MAP:
|
|
117
|
+
OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
|
119
118
|
host_pb2.PUBLIC: OwnAddressType.PUBLIC,
|
|
120
119
|
host_pb2.RANDOM: OwnAddressType.RANDOM,
|
|
121
120
|
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
|
|
@@ -124,7 +123,7 @@ OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
class HostService(HostServicer):
|
|
127
|
-
waited_connections:
|
|
126
|
+
waited_connections: set[int]
|
|
128
127
|
|
|
129
128
|
def __init__(
|
|
130
129
|
self, grpc_server: grpc.aio.Server, device: Device, config: Config
|
|
@@ -296,12 +295,12 @@ class HostService(HostServicer):
|
|
|
296
295
|
def on_disconnection(_: None) -> None:
|
|
297
296
|
disconnection_future.set_result(None)
|
|
298
297
|
|
|
299
|
-
connection.on(
|
|
298
|
+
connection.on(connection.EVENT_DISCONNECTION, on_disconnection)
|
|
300
299
|
try:
|
|
301
300
|
await disconnection_future
|
|
302
301
|
self.log.debug("Disconnected")
|
|
303
302
|
finally:
|
|
304
|
-
connection.remove_listener(
|
|
303
|
+
connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore
|
|
305
304
|
|
|
306
305
|
return empty_pb2.Empty()
|
|
307
306
|
|
|
@@ -383,7 +382,7 @@ class HostService(HostServicer):
|
|
|
383
382
|
):
|
|
384
383
|
connections.put_nowait(connection)
|
|
385
384
|
|
|
386
|
-
self.device.on(
|
|
385
|
+
self.device.on(self.device.EVENT_CONNECTION, on_connection)
|
|
387
386
|
|
|
388
387
|
try:
|
|
389
388
|
# Advertise until RPC is canceled
|
|
@@ -501,7 +500,7 @@ class HostService(HostServicer):
|
|
|
501
500
|
):
|
|
502
501
|
connections.put_nowait(connection)
|
|
503
502
|
|
|
504
|
-
self.device.on(
|
|
503
|
+
self.device.on(self.device.EVENT_CONNECTION, on_connection)
|
|
505
504
|
|
|
506
505
|
try:
|
|
507
506
|
while True:
|
|
@@ -531,7 +530,7 @@ class HostService(HostServicer):
|
|
|
531
530
|
await asyncio.sleep(1)
|
|
532
531
|
finally:
|
|
533
532
|
if request.connectable:
|
|
534
|
-
self.device.remove_listener(
|
|
533
|
+
self.device.remove_listener(self.device.EVENT_CONNECTION, on_connection) # type: ignore
|
|
535
534
|
|
|
536
535
|
try:
|
|
537
536
|
self.log.debug('Stop advertising')
|
|
@@ -557,7 +556,7 @@ class HostService(HostServicer):
|
|
|
557
556
|
scanning_phys = [int(Phy.LE_1M), int(Phy.LE_CODED)]
|
|
558
557
|
|
|
559
558
|
scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
|
|
560
|
-
handler = self.device.on(
|
|
559
|
+
handler = self.device.on(self.device.EVENT_ADVERTISEMENT, scan_queue.put_nowait)
|
|
561
560
|
await self.device.start_scanning(
|
|
562
561
|
legacy=request.legacy,
|
|
563
562
|
active=not request.passive,
|
|
@@ -602,7 +601,7 @@ class HostService(HostServicer):
|
|
|
602
601
|
yield sr
|
|
603
602
|
|
|
604
603
|
finally:
|
|
605
|
-
self.device.remove_listener(
|
|
604
|
+
self.device.remove_listener(self.device.EVENT_ADVERTISEMENT, handler) # type: ignore
|
|
606
605
|
try:
|
|
607
606
|
self.log.debug('Stop scanning')
|
|
608
607
|
await bumble.utils.cancel_on_event(
|
|
@@ -618,13 +617,13 @@ class HostService(HostServicer):
|
|
|
618
617
|
self.log.debug('Inquiry')
|
|
619
618
|
|
|
620
619
|
inquiry_queue: asyncio.Queue[
|
|
621
|
-
Optional[
|
|
620
|
+
Optional[tuple[Address, int, AdvertisingData, int]]
|
|
622
621
|
] = asyncio.Queue()
|
|
623
622
|
complete_handler = self.device.on(
|
|
624
|
-
|
|
623
|
+
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
|
625
624
|
)
|
|
626
625
|
result_handler = self.device.on( # type: ignore
|
|
627
|
-
|
|
626
|
+
self.device.EVENT_INQUIRY_RESULT,
|
|
628
627
|
lambda address, class_of_device, eir_data, rssi: inquiry_queue.put_nowait( # type: ignore
|
|
629
628
|
(address, class_of_device, eir_data, rssi) # type: ignore
|
|
630
629
|
),
|
|
@@ -643,8 +642,8 @@ class HostService(HostServicer):
|
|
|
643
642
|
)
|
|
644
643
|
|
|
645
644
|
finally:
|
|
646
|
-
self.device.remove_listener(
|
|
647
|
-
self.device.remove_listener(
|
|
645
|
+
self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore
|
|
646
|
+
self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore
|
|
648
647
|
try:
|
|
649
648
|
self.log.debug('Stop inquiry')
|
|
650
649
|
await bumble.utils.cancel_on_event(
|
|
@@ -670,10 +669,10 @@ class HostService(HostServicer):
|
|
|
670
669
|
return empty_pb2.Empty()
|
|
671
670
|
|
|
672
671
|
def unpack_data_types(self, dt: DataTypes) -> AdvertisingData:
|
|
673
|
-
ad_structures:
|
|
672
|
+
ad_structures: list[tuple[int, bytes]] = []
|
|
674
673
|
|
|
675
|
-
uuids:
|
|
676
|
-
datas:
|
|
674
|
+
uuids: list[str]
|
|
675
|
+
datas: dict[str, bytes]
|
|
677
676
|
|
|
678
677
|
def uuid128_from_str(uuid: str) -> bytes:
|
|
679
678
|
"""Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
|
@@ -887,50 +886,50 @@ class HostService(HostServicer):
|
|
|
887
886
|
|
|
888
887
|
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
|
|
889
888
|
dt = DataTypes()
|
|
890
|
-
uuids:
|
|
889
|
+
uuids: list[UUID]
|
|
891
890
|
s: str
|
|
892
891
|
i: int
|
|
893
|
-
ij:
|
|
894
|
-
uuid_data:
|
|
892
|
+
ij: tuple[int, int]
|
|
893
|
+
uuid_data: tuple[UUID, bytes]
|
|
895
894
|
data: bytes
|
|
896
895
|
|
|
897
896
|
if uuids := cast(
|
|
898
|
-
|
|
897
|
+
list[UUID],
|
|
899
898
|
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
|
|
900
899
|
):
|
|
901
900
|
dt.incomplete_service_class_uuids16.extend(
|
|
902
901
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
903
902
|
)
|
|
904
903
|
if uuids := cast(
|
|
905
|
-
|
|
904
|
+
list[UUID],
|
|
906
905
|
ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
|
|
907
906
|
):
|
|
908
907
|
dt.complete_service_class_uuids16.extend(
|
|
909
908
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
910
909
|
)
|
|
911
910
|
if uuids := cast(
|
|
912
|
-
|
|
911
|
+
list[UUID],
|
|
913
912
|
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
|
|
914
913
|
):
|
|
915
914
|
dt.incomplete_service_class_uuids32.extend(
|
|
916
915
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
917
916
|
)
|
|
918
917
|
if uuids := cast(
|
|
919
|
-
|
|
918
|
+
list[UUID],
|
|
920
919
|
ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
|
|
921
920
|
):
|
|
922
921
|
dt.complete_service_class_uuids32.extend(
|
|
923
922
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
924
923
|
)
|
|
925
924
|
if uuids := cast(
|
|
926
|
-
|
|
925
|
+
list[UUID],
|
|
927
926
|
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
|
|
928
927
|
):
|
|
929
928
|
dt.incomplete_service_class_uuids128.extend(
|
|
930
929
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
931
930
|
)
|
|
932
931
|
if uuids := cast(
|
|
933
|
-
|
|
932
|
+
list[UUID],
|
|
934
933
|
ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
|
|
935
934
|
):
|
|
936
935
|
dt.complete_service_class_uuids128.extend(
|
|
@@ -945,42 +944,42 @@ class HostService(HostServicer):
|
|
|
945
944
|
if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)):
|
|
946
945
|
dt.class_of_device = i
|
|
947
946
|
if ij := cast(
|
|
948
|
-
|
|
947
|
+
tuple[int, int],
|
|
949
948
|
ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE),
|
|
950
949
|
):
|
|
951
950
|
dt.peripheral_connection_interval_min = ij[0]
|
|
952
951
|
dt.peripheral_connection_interval_max = ij[1]
|
|
953
952
|
if uuids := cast(
|
|
954
|
-
|
|
953
|
+
list[UUID],
|
|
955
954
|
ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS),
|
|
956
955
|
):
|
|
957
956
|
dt.service_solicitation_uuids16.extend(
|
|
958
957
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
959
958
|
)
|
|
960
959
|
if uuids := cast(
|
|
961
|
-
|
|
960
|
+
list[UUID],
|
|
962
961
|
ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS),
|
|
963
962
|
):
|
|
964
963
|
dt.service_solicitation_uuids32.extend(
|
|
965
964
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
966
965
|
)
|
|
967
966
|
if uuids := cast(
|
|
968
|
-
|
|
967
|
+
list[UUID],
|
|
969
968
|
ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS),
|
|
970
969
|
):
|
|
971
970
|
dt.service_solicitation_uuids128.extend(
|
|
972
971
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
973
972
|
)
|
|
974
973
|
if uuid_data := cast(
|
|
975
|
-
|
|
974
|
+
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
|
|
976
975
|
):
|
|
977
976
|
dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
|
978
977
|
if uuid_data := cast(
|
|
979
|
-
|
|
978
|
+
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
|
|
980
979
|
):
|
|
981
980
|
dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
|
982
981
|
if uuid_data := cast(
|
|
983
|
-
|
|
982
|
+
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
|
|
984
983
|
):
|
|
985
984
|
dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
|
986
985
|
if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)):
|
bumble/pandora/l2cap.py
CHANGED
|
@@ -51,7 +51,7 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
|
|
|
51
51
|
WaitDisconnectionRequest,
|
|
52
52
|
WaitDisconnectionResponse,
|
|
53
53
|
)
|
|
54
|
-
from typing import AsyncGenerator,
|
|
54
|
+
from typing import AsyncGenerator, Optional, Union
|
|
55
55
|
from dataclasses import dataclass
|
|
56
56
|
|
|
57
57
|
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
|
|
@@ -70,7 +70,7 @@ class L2CAPService(L2CAPServicer):
|
|
|
70
70
|
)
|
|
71
71
|
self.device = device
|
|
72
72
|
self.config = config
|
|
73
|
-
self.channels:
|
|
73
|
+
self.channels: dict[bytes, ChannelContext] = {}
|
|
74
74
|
|
|
75
75
|
def register_event(self, l2cap_channel: L2capChannel) -> ChannelContext:
|
|
76
76
|
close_future = asyncio.get_running_loop().create_future()
|
|
@@ -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
|
|
@@ -55,7 +57,7 @@ from pandora.security_pb2 import (
|
|
|
55
57
|
WaitSecurityRequest,
|
|
56
58
|
WaitSecurityResponse,
|
|
57
59
|
)
|
|
58
|
-
from typing import Any, AsyncGenerator, AsyncIterator, Callable,
|
|
60
|
+
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
class PairingDelegate(BasePairingDelegate):
|
|
@@ -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.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
|
248
|
+
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
if level == LEVEL4:
|
|
252
|
+
return (
|
|
253
|
+
connection.encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
|
254
|
+
and connection.authenticated
|
|
255
|
+
and link_key_type
|
|
256
|
+
== hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256
|
|
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:
|
|
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/pandora/utils.py
CHANGED
|
@@ -22,9 +22,9 @@ import logging
|
|
|
22
22
|
from bumble.device import Device
|
|
23
23
|
from bumble.hci import Address, AddressType
|
|
24
24
|
from google.protobuf.message import Message # pytype: disable=pyi-error
|
|
25
|
-
from typing import Any,
|
|
25
|
+
from typing import Any, Generator, MutableMapping, Optional
|
|
26
26
|
|
|
27
|
-
ADDRESS_TYPES:
|
|
27
|
+
ADDRESS_TYPES: dict[str, AddressType] = {
|
|
28
28
|
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
|
29
29
|
"random": Address.RANDOM_DEVICE_ADDRESS,
|
|
30
30
|
"public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
|
|
@@ -43,7 +43,7 @@ class BumbleServerLoggerAdapter(logging.LoggerAdapter): # type: ignore
|
|
|
43
43
|
|
|
44
44
|
def process(
|
|
45
45
|
self, msg: str, kwargs: MutableMapping[str, Any]
|
|
46
|
-
) ->
|
|
46
|
+
) -> tuple[str, MutableMapping[str, Any]]:
|
|
47
47
|
assert self.extra
|
|
48
48
|
service_name = self.extra['service_name']
|
|
49
49
|
assert isinstance(service_name, str)
|
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
|
bumble/profiles/ancs.py
CHANGED
|
@@ -250,6 +250,8 @@ class AncsClient(utils.EventEmitter):
|
|
|
250
250
|
_expected_response_tuples: int
|
|
251
251
|
_response_accumulator: bytes
|
|
252
252
|
|
|
253
|
+
EVENT_NOTIFICATION = "notification"
|
|
254
|
+
|
|
253
255
|
def __init__(self, ancs_proxy: AncsProxy) -> None:
|
|
254
256
|
super().__init__()
|
|
255
257
|
self._ancs_proxy = ancs_proxy
|
|
@@ -284,7 +286,7 @@ class AncsClient(utils.EventEmitter):
|
|
|
284
286
|
|
|
285
287
|
def _on_notification(self, notification: Notification) -> None:
|
|
286
288
|
logger.debug(f"ANCS NOTIFICATION: {notification}")
|
|
287
|
-
self.emit(
|
|
289
|
+
self.emit(self.EVENT_NOTIFICATION, notification)
|
|
288
290
|
|
|
289
291
|
def _on_data(self, data: bytes) -> None:
|
|
290
292
|
logger.debug(f"ANCS DATA: {data.hex()}")
|