bumble 0.0.178__py3-none-any.whl → 0.0.180__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 +83 -68
- bumble/apps/bench.py +180 -24
- bumble/apps/controller_info.py +14 -0
- bumble/apps/pair.py +9 -2
- bumble/avdtp.py +3 -3
- bumble/crypto.py +82 -66
- bumble/device.py +247 -23
- bumble/gatt.py +117 -7
- bumble/gatt_client.py +56 -20
- bumble/hci.py +351 -78
- bumble/helpers.py +67 -42
- bumble/hid.py +8 -7
- bumble/l2cap.py +8 -0
- bumble/profiles/csip.py +147 -0
- bumble/rfcomm.py +2 -3
- bumble/sdp.py +4 -4
- bumble/smp.py +66 -43
- bumble/transport/common.py +1 -1
- bumble/transport/usb.py +58 -61
- bumble/utils.py +17 -1
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/METADATA +1 -1
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/RECORD +27 -26
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/WHEEL +1 -1
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/LICENSE +0 -0
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/top_level.txt +0 -0
bumble/helpers.py
CHANGED
|
@@ -15,30 +15,39 @@
|
|
|
15
15
|
# -----------------------------------------------------------------------------
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Callable, MutableMapping
|
|
21
|
+
from typing import cast, Any
|
|
18
22
|
import logging
|
|
19
23
|
|
|
20
|
-
from
|
|
21
|
-
from .
|
|
22
|
-
from .
|
|
23
|
-
from .
|
|
24
|
-
from .
|
|
24
|
+
from bumble import avdtp
|
|
25
|
+
from bumble.colors import color
|
|
26
|
+
from bumble.att import ATT_CID, ATT_PDU
|
|
27
|
+
from bumble.smp import SMP_CID, SMP_Command
|
|
28
|
+
from bumble.core import name_or_number
|
|
29
|
+
from bumble.l2cap import (
|
|
25
30
|
L2CAP_PDU,
|
|
26
31
|
L2CAP_CONNECTION_REQUEST,
|
|
27
32
|
L2CAP_CONNECTION_RESPONSE,
|
|
28
33
|
L2CAP_SIGNALING_CID,
|
|
29
34
|
L2CAP_LE_SIGNALING_CID,
|
|
30
35
|
L2CAP_Control_Frame,
|
|
36
|
+
L2CAP_Connection_Request,
|
|
31
37
|
L2CAP_Connection_Response,
|
|
32
38
|
)
|
|
33
|
-
from .hci import (
|
|
39
|
+
from bumble.hci import (
|
|
34
40
|
HCI_EVENT_PACKET,
|
|
35
41
|
HCI_ACL_DATA_PACKET,
|
|
36
42
|
HCI_DISCONNECTION_COMPLETE_EVENT,
|
|
37
43
|
HCI_AclDataPacketAssembler,
|
|
44
|
+
HCI_Packet,
|
|
45
|
+
HCI_Event,
|
|
46
|
+
HCI_AclDataPacket,
|
|
47
|
+
HCI_Disconnection_Complete_Event,
|
|
38
48
|
)
|
|
39
|
-
from .rfcomm import RFCOMM_Frame, RFCOMM_PSM
|
|
40
|
-
from .sdp import SDP_PDU, SDP_PSM
|
|
41
|
-
from .avdtp import MessageAssembler as AVDTP_MessageAssembler, AVDTP_PSM
|
|
49
|
+
from bumble.rfcomm import RFCOMM_Frame, RFCOMM_PSM
|
|
50
|
+
from bumble.sdp import SDP_PDU, SDP_PSM
|
|
42
51
|
|
|
43
52
|
# -----------------------------------------------------------------------------
|
|
44
53
|
# Logging
|
|
@@ -50,23 +59,25 @@ logger = logging.getLogger(__name__)
|
|
|
50
59
|
PSM_NAMES = {
|
|
51
60
|
RFCOMM_PSM: 'RFCOMM',
|
|
52
61
|
SDP_PSM: 'SDP',
|
|
53
|
-
AVDTP_PSM: 'AVDTP'
|
|
54
|
-
# TODO: add more PSM values
|
|
62
|
+
avdtp.AVDTP_PSM: 'AVDTP',
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
# -----------------------------------------------------------------------------
|
|
59
67
|
class PacketTracer:
|
|
60
68
|
class AclStream:
|
|
61
|
-
|
|
69
|
+
psms: MutableMapping[int, int]
|
|
70
|
+
peer: PacketTracer.AclStream
|
|
71
|
+
avdtp_assemblers: MutableMapping[int, avdtp.MessageAssembler]
|
|
72
|
+
|
|
73
|
+
def __init__(self, analyzer: PacketTracer.Analyzer) -> None:
|
|
62
74
|
self.analyzer = analyzer
|
|
63
75
|
self.packet_assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
|
64
76
|
self.avdtp_assemblers = {} # AVDTP assemblers, by source_cid
|
|
65
77
|
self.psms = {} # PSM, by source_cid
|
|
66
|
-
self.peer = None # ACL stream in the other direction
|
|
67
78
|
|
|
68
79
|
# pylint: disable=too-many-nested-blocks
|
|
69
|
-
def on_acl_pdu(self, pdu):
|
|
80
|
+
def on_acl_pdu(self, pdu: bytes) -> None:
|
|
70
81
|
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
|
71
82
|
|
|
72
83
|
if l2cap_pdu.cid == ATT_CID:
|
|
@@ -81,26 +92,30 @@ class PacketTracer:
|
|
|
81
92
|
|
|
82
93
|
# Check if this signals a new channel
|
|
83
94
|
if control_frame.code == L2CAP_CONNECTION_REQUEST:
|
|
84
|
-
|
|
95
|
+
connection_request = cast(L2CAP_Connection_Request, control_frame)
|
|
96
|
+
self.psms[connection_request.source_cid] = connection_request.psm
|
|
85
97
|
elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
|
|
98
|
+
connection_response = cast(L2CAP_Connection_Response, control_frame)
|
|
86
99
|
if (
|
|
87
|
-
|
|
100
|
+
connection_response.result
|
|
88
101
|
== L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
|
|
89
102
|
):
|
|
90
103
|
if self.peer:
|
|
91
|
-
if psm := self.peer.psms.get(
|
|
104
|
+
if psm := self.peer.psms.get(
|
|
105
|
+
connection_response.source_cid
|
|
106
|
+
):
|
|
92
107
|
# Found a pending connection
|
|
93
|
-
self.psms[
|
|
108
|
+
self.psms[connection_response.destination_cid] = psm
|
|
94
109
|
|
|
95
110
|
# For AVDTP connections, create a packet assembler for
|
|
96
111
|
# each direction
|
|
97
|
-
if psm == AVDTP_PSM:
|
|
112
|
+
if psm == avdtp.AVDTP_PSM:
|
|
98
113
|
self.avdtp_assemblers[
|
|
99
|
-
|
|
100
|
-
] =
|
|
114
|
+
connection_response.source_cid
|
|
115
|
+
] = avdtp.MessageAssembler(self.on_avdtp_message)
|
|
101
116
|
self.peer.avdtp_assemblers[
|
|
102
|
-
|
|
103
|
-
] =
|
|
117
|
+
connection_response.destination_cid
|
|
118
|
+
] = avdtp.MessageAssembler(
|
|
104
119
|
self.peer.on_avdtp_message
|
|
105
120
|
)
|
|
106
121
|
|
|
@@ -113,7 +128,7 @@ class PacketTracer:
|
|
|
113
128
|
elif psm == RFCOMM_PSM:
|
|
114
129
|
rfcomm_frame = RFCOMM_Frame.from_bytes(l2cap_pdu.payload)
|
|
115
130
|
self.analyzer.emit(rfcomm_frame)
|
|
116
|
-
elif psm == AVDTP_PSM:
|
|
131
|
+
elif psm == avdtp.AVDTP_PSM:
|
|
117
132
|
self.analyzer.emit(
|
|
118
133
|
f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
|
|
119
134
|
f'PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
|
|
@@ -130,22 +145,26 @@ class PacketTracer:
|
|
|
130
145
|
else:
|
|
131
146
|
self.analyzer.emit(l2cap_pdu)
|
|
132
147
|
|
|
133
|
-
def on_avdtp_message(
|
|
148
|
+
def on_avdtp_message(
|
|
149
|
+
self, transaction_label: int, message: avdtp.Message
|
|
150
|
+
) -> None:
|
|
134
151
|
self.analyzer.emit(
|
|
135
152
|
f'{color("AVDTP", "green")} [{transaction_label}] {message}'
|
|
136
153
|
)
|
|
137
154
|
|
|
138
|
-
def feed_packet(self, packet):
|
|
155
|
+
def feed_packet(self, packet: HCI_AclDataPacket) -> None:
|
|
139
156
|
self.packet_assembler.feed_packet(packet)
|
|
140
157
|
|
|
141
158
|
class Analyzer:
|
|
142
|
-
|
|
159
|
+
acl_streams: MutableMapping[int, PacketTracer.AclStream]
|
|
160
|
+
peer: PacketTracer.Analyzer
|
|
161
|
+
|
|
162
|
+
def __init__(self, label: str, emit_message: Callable[..., None]) -> None:
|
|
143
163
|
self.label = label
|
|
144
164
|
self.emit_message = emit_message
|
|
145
165
|
self.acl_streams = {} # ACL streams, by connection handle
|
|
146
|
-
self.peer = None # Analyzer in the other direction
|
|
147
166
|
|
|
148
|
-
def start_acl_stream(self, connection_handle):
|
|
167
|
+
def start_acl_stream(self, connection_handle: int) -> PacketTracer.AclStream:
|
|
149
168
|
logger.info(
|
|
150
169
|
f'[{self.label}] +++ Creating ACL stream for connection '
|
|
151
170
|
f'0x{connection_handle:04X}'
|
|
@@ -160,7 +179,7 @@ class PacketTracer:
|
|
|
160
179
|
|
|
161
180
|
return stream
|
|
162
181
|
|
|
163
|
-
def end_acl_stream(self, connection_handle):
|
|
182
|
+
def end_acl_stream(self, connection_handle: int) -> None:
|
|
164
183
|
if connection_handle in self.acl_streams:
|
|
165
184
|
logger.info(
|
|
166
185
|
f'[{self.label}] --- Removing ACL stream for connection '
|
|
@@ -171,23 +190,29 @@ class PacketTracer:
|
|
|
171
190
|
# Let the other forwarder know so it can cleanup its stream as well
|
|
172
191
|
self.peer.end_acl_stream(connection_handle)
|
|
173
192
|
|
|
174
|
-
def on_packet(self, packet):
|
|
193
|
+
def on_packet(self, packet: HCI_Packet) -> None:
|
|
175
194
|
self.emit(packet)
|
|
176
195
|
|
|
177
196
|
if packet.hci_packet_type == HCI_ACL_DATA_PACKET:
|
|
197
|
+
acl_packet = cast(HCI_AclDataPacket, packet)
|
|
178
198
|
# Look for an existing stream for this handle, create one if it is the
|
|
179
199
|
# first ACL packet for that connection handle
|
|
180
|
-
if (
|
|
181
|
-
stream
|
|
182
|
-
|
|
200
|
+
if (
|
|
201
|
+
stream := self.acl_streams.get(acl_packet.connection_handle)
|
|
202
|
+
) is None:
|
|
203
|
+
stream = self.start_acl_stream(acl_packet.connection_handle)
|
|
204
|
+
stream.feed_packet(acl_packet)
|
|
183
205
|
elif packet.hci_packet_type == HCI_EVENT_PACKET:
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
event_packet = cast(HCI_Event, packet)
|
|
207
|
+
if event_packet.event_code == HCI_DISCONNECTION_COMPLETE_EVENT:
|
|
208
|
+
self.end_acl_stream(
|
|
209
|
+
cast(HCI_Disconnection_Complete_Event, packet).connection_handle
|
|
210
|
+
)
|
|
186
211
|
|
|
187
|
-
def emit(self, message):
|
|
212
|
+
def emit(self, message: Any) -> None:
|
|
188
213
|
self.emit_message(f'[{self.label}] {message}')
|
|
189
214
|
|
|
190
|
-
def trace(self, packet, direction=0):
|
|
215
|
+
def trace(self, packet: HCI_Packet, direction: int = 0) -> None:
|
|
191
216
|
if direction == 0:
|
|
192
217
|
self.host_to_controller_analyzer.on_packet(packet)
|
|
193
218
|
else:
|
|
@@ -195,10 +220,10 @@ class PacketTracer:
|
|
|
195
220
|
|
|
196
221
|
def __init__(
|
|
197
222
|
self,
|
|
198
|
-
host_to_controller_label=color('HOST->CONTROLLER', 'blue'),
|
|
199
|
-
controller_to_host_label=color('CONTROLLER->HOST', 'cyan'),
|
|
200
|
-
emit_message=logger.info,
|
|
201
|
-
):
|
|
223
|
+
host_to_controller_label: str = color('HOST->CONTROLLER', 'blue'),
|
|
224
|
+
controller_to_host_label: str = color('CONTROLLER->HOST', 'cyan'),
|
|
225
|
+
emit_message: Callable[..., None] = logger.info,
|
|
226
|
+
) -> None:
|
|
202
227
|
self.host_to_controller_analyzer = PacketTracer.Analyzer(
|
|
203
228
|
host_to_controller_label, emit_message
|
|
204
229
|
)
|
bumble/hid.py
CHANGED
|
@@ -18,15 +18,14 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
from dataclasses import dataclass
|
|
20
20
|
import logging
|
|
21
|
-
import asyncio
|
|
22
21
|
import enum
|
|
23
22
|
|
|
24
23
|
from pyee import EventEmitter
|
|
25
|
-
from typing import Optional,
|
|
24
|
+
from typing import Optional, TYPE_CHECKING
|
|
26
25
|
|
|
27
|
-
from
|
|
28
|
-
from .colors import color
|
|
29
|
-
from .core import
|
|
26
|
+
from bumble import l2cap
|
|
27
|
+
from bumble.colors import color
|
|
28
|
+
from bumble.core import InvalidStateError, ProtocolError
|
|
30
29
|
|
|
31
30
|
if TYPE_CHECKING:
|
|
32
31
|
from bumble.device import Device, Connection
|
|
@@ -302,10 +301,12 @@ class Host(EventEmitter):
|
|
|
302
301
|
self.send_pdu_on_ctrl(hid_message)
|
|
303
302
|
|
|
304
303
|
def send_pdu_on_ctrl(self, msg: bytes) -> None:
|
|
305
|
-
self.l2cap_ctrl_channel
|
|
304
|
+
assert self.l2cap_ctrl_channel
|
|
305
|
+
self.l2cap_ctrl_channel.send_pdu(msg)
|
|
306
306
|
|
|
307
307
|
def send_pdu_on_intr(self, msg: bytes) -> None:
|
|
308
|
-
self.l2cap_intr_channel
|
|
308
|
+
assert self.l2cap_intr_channel
|
|
309
|
+
self.l2cap_intr_channel.send_pdu(msg)
|
|
309
310
|
|
|
310
311
|
def send_data(self, data):
|
|
311
312
|
msg = SendData(data)
|
bumble/l2cap.py
CHANGED
|
@@ -391,6 +391,9 @@ class L2CAP_Connection_Request(L2CAP_Control_Frame):
|
|
|
391
391
|
See Bluetooth spec @ Vol 3, Part A - 4.2 CONNECTION REQUEST
|
|
392
392
|
'''
|
|
393
393
|
|
|
394
|
+
psm: int
|
|
395
|
+
source_cid: int
|
|
396
|
+
|
|
394
397
|
@staticmethod
|
|
395
398
|
def parse_psm(data: bytes, offset: int = 0) -> Tuple[int, int]:
|
|
396
399
|
psm_length = 2
|
|
@@ -432,6 +435,11 @@ class L2CAP_Connection_Response(L2CAP_Control_Frame):
|
|
|
432
435
|
See Bluetooth spec @ Vol 3, Part A - 4.3 CONNECTION RESPONSE
|
|
433
436
|
'''
|
|
434
437
|
|
|
438
|
+
source_cid: int
|
|
439
|
+
destination_cid: int
|
|
440
|
+
status: int
|
|
441
|
+
result: int
|
|
442
|
+
|
|
435
443
|
CONNECTION_SUCCESSFUL = 0x0000
|
|
436
444
|
CONNECTION_PENDING = 0x0001
|
|
437
445
|
CONNECTION_REFUSED_PSM_NOT_SUPPORTED = 0x0002
|
bumble/profiles/csip.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright 2021-2023 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# -----------------------------------------------------------------------------
|
|
17
|
+
# Imports
|
|
18
|
+
# -----------------------------------------------------------------------------
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import enum
|
|
21
|
+
import struct
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from bumble import gatt
|
|
25
|
+
from bumble import gatt_client
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# -----------------------------------------------------------------------------
|
|
29
|
+
# Constants
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
class SirkType(enum.IntEnum):
|
|
32
|
+
'''Coordinated Set Identification Service - 5.1 Set Identity Resolving Key.'''
|
|
33
|
+
|
|
34
|
+
ENCRYPTED = 0x00
|
|
35
|
+
PLAINTEXT = 0x01
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MemberLock(enum.IntEnum):
|
|
39
|
+
'''Coordinated Set Identification Service - 5.3 Set Member Lock.'''
|
|
40
|
+
|
|
41
|
+
UNLOCKED = 0x01
|
|
42
|
+
LOCKED = 0x02
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# -----------------------------------------------------------------------------
|
|
46
|
+
# Utils
|
|
47
|
+
# -----------------------------------------------------------------------------
|
|
48
|
+
# TODO: Implement RSI Generator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# -----------------------------------------------------------------------------
|
|
52
|
+
# Server
|
|
53
|
+
# -----------------------------------------------------------------------------
|
|
54
|
+
class CoordinatedSetIdentificationService(gatt.TemplateService):
|
|
55
|
+
UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
|
|
56
|
+
|
|
57
|
+
set_identity_resolving_key_characteristic: gatt.Characteristic
|
|
58
|
+
coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
|
|
59
|
+
set_member_lock_characteristic: Optional[gatt.Characteristic] = None
|
|
60
|
+
set_member_rank_characteristic: Optional[gatt.Characteristic] = None
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
set_identity_resolving_key: bytes,
|
|
65
|
+
coordinated_set_size: Optional[int] = None,
|
|
66
|
+
set_member_lock: Optional[MemberLock] = None,
|
|
67
|
+
set_member_rank: Optional[int] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
characteristics = []
|
|
70
|
+
|
|
71
|
+
self.set_identity_resolving_key_characteristic = gatt.Characteristic(
|
|
72
|
+
uuid=gatt.GATT_SET_IDENTITY_RESOLVING_KEY_CHARACTERISTIC,
|
|
73
|
+
properties=gatt.Characteristic.Properties.READ
|
|
74
|
+
| gatt.Characteristic.Properties.NOTIFY,
|
|
75
|
+
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
76
|
+
# TODO: Implement encrypted SIRK reader.
|
|
77
|
+
value=struct.pack('B', SirkType.PLAINTEXT) + set_identity_resolving_key,
|
|
78
|
+
)
|
|
79
|
+
characteristics.append(self.set_identity_resolving_key_characteristic)
|
|
80
|
+
|
|
81
|
+
if coordinated_set_size is not None:
|
|
82
|
+
self.coordinated_set_size_characteristic = gatt.Characteristic(
|
|
83
|
+
uuid=gatt.GATT_COORDINATED_SET_SIZE_CHARACTERISTIC,
|
|
84
|
+
properties=gatt.Characteristic.Properties.READ
|
|
85
|
+
| gatt.Characteristic.Properties.NOTIFY,
|
|
86
|
+
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
87
|
+
value=struct.pack('B', coordinated_set_size),
|
|
88
|
+
)
|
|
89
|
+
characteristics.append(self.coordinated_set_size_characteristic)
|
|
90
|
+
|
|
91
|
+
if set_member_lock is not None:
|
|
92
|
+
self.set_member_lock_characteristic = gatt.Characteristic(
|
|
93
|
+
uuid=gatt.GATT_SET_MEMBER_LOCK_CHARACTERISTIC,
|
|
94
|
+
properties=gatt.Characteristic.Properties.READ
|
|
95
|
+
| gatt.Characteristic.Properties.NOTIFY
|
|
96
|
+
| gatt.Characteristic.Properties.WRITE,
|
|
97
|
+
permissions=gatt.Characteristic.Permissions.READABLE
|
|
98
|
+
| gatt.Characteristic.Permissions.WRITEABLE,
|
|
99
|
+
value=struct.pack('B', set_member_lock),
|
|
100
|
+
)
|
|
101
|
+
characteristics.append(self.set_member_lock_characteristic)
|
|
102
|
+
|
|
103
|
+
if set_member_rank is not None:
|
|
104
|
+
self.set_member_rank_characteristic = gatt.Characteristic(
|
|
105
|
+
uuid=gatt.GATT_SET_MEMBER_RANK_CHARACTERISTIC,
|
|
106
|
+
properties=gatt.Characteristic.Properties.READ
|
|
107
|
+
| gatt.Characteristic.Properties.NOTIFY,
|
|
108
|
+
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
109
|
+
value=struct.pack('B', set_member_rank),
|
|
110
|
+
)
|
|
111
|
+
characteristics.append(self.set_member_rank_characteristic)
|
|
112
|
+
|
|
113
|
+
super().__init__(characteristics)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# -----------------------------------------------------------------------------
|
|
117
|
+
# Client
|
|
118
|
+
# -----------------------------------------------------------------------------
|
|
119
|
+
class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
|
120
|
+
SERVICE_CLASS = CoordinatedSetIdentificationService
|
|
121
|
+
|
|
122
|
+
set_identity_resolving_key: gatt_client.CharacteristicProxy
|
|
123
|
+
coordinated_set_size: Optional[gatt_client.CharacteristicProxy] = None
|
|
124
|
+
set_member_lock: Optional[gatt_client.CharacteristicProxy] = None
|
|
125
|
+
set_member_rank: Optional[gatt_client.CharacteristicProxy] = None
|
|
126
|
+
|
|
127
|
+
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
|
128
|
+
self.service_proxy = service_proxy
|
|
129
|
+
|
|
130
|
+
self.set_identity_resolving_key = service_proxy.get_characteristics_by_uuid(
|
|
131
|
+
gatt.GATT_SET_IDENTITY_RESOLVING_KEY_CHARACTERISTIC
|
|
132
|
+
)[0]
|
|
133
|
+
|
|
134
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
135
|
+
gatt.GATT_COORDINATED_SET_SIZE_CHARACTERISTIC
|
|
136
|
+
):
|
|
137
|
+
self.coordinated_set_size = characteristics[0]
|
|
138
|
+
|
|
139
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
140
|
+
gatt.GATT_SET_MEMBER_LOCK_CHARACTERISTIC
|
|
141
|
+
):
|
|
142
|
+
self.set_member_lock = characteristics[0]
|
|
143
|
+
|
|
144
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
145
|
+
gatt.GATT_SET_MEMBER_RANK_CHARACTERISTIC
|
|
146
|
+
):
|
|
147
|
+
self.set_member_rank = characteristics[0]
|
bumble/rfcomm.py
CHANGED
|
@@ -889,8 +889,7 @@ class Client:
|
|
|
889
889
|
multiplexer: Optional[Multiplexer]
|
|
890
890
|
l2cap_channel: Optional[l2cap.ClassicChannel]
|
|
891
891
|
|
|
892
|
-
def __init__(self,
|
|
893
|
-
self.device = device
|
|
892
|
+
def __init__(self, connection: Connection) -> None:
|
|
894
893
|
self.connection = connection
|
|
895
894
|
self.l2cap_channel = None
|
|
896
895
|
self.multiplexer = None
|
|
@@ -906,7 +905,7 @@ class Client:
|
|
|
906
905
|
raise
|
|
907
906
|
|
|
908
907
|
assert self.l2cap_channel is not None
|
|
909
|
-
# Create a
|
|
908
|
+
# Create a multiplexer to manage DLCs with the server
|
|
910
909
|
self.multiplexer = Multiplexer(self.l2cap_channel, Multiplexer.Role.INITIATOR)
|
|
911
910
|
|
|
912
911
|
# Connect the multiplexer
|
bumble/sdp.py
CHANGED
|
@@ -760,13 +760,13 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
|
|
|
760
760
|
class Client:
|
|
761
761
|
channel: Optional[l2cap.ClassicChannel]
|
|
762
762
|
|
|
763
|
-
def __init__(self,
|
|
764
|
-
self.
|
|
763
|
+
def __init__(self, connection: Connection) -> None:
|
|
764
|
+
self.connection = connection
|
|
765
765
|
self.pending_request = None
|
|
766
766
|
self.channel = None
|
|
767
767
|
|
|
768
|
-
async def connect(self
|
|
769
|
-
self.channel = await connection.create_l2cap_channel(
|
|
768
|
+
async def connect(self) -> None:
|
|
769
|
+
self.channel = await self.connection.create_l2cap_channel(
|
|
770
770
|
spec=l2cap.ClassicChannelSpec(SDP_PSM)
|
|
771
771
|
)
|
|
772
772
|
|
bumble/smp.py
CHANGED
|
@@ -187,8 +187,8 @@ SMP_KEYPRESS_AUTHREQ = 0b00010000
|
|
|
187
187
|
SMP_CT2_AUTHREQ = 0b00100000
|
|
188
188
|
|
|
189
189
|
# Crypto salt
|
|
190
|
-
SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('
|
|
191
|
-
SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('
|
|
190
|
+
SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('000000000000000000000000746D7031')
|
|
191
|
+
SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('000000000000000000000000746D7032')
|
|
192
192
|
|
|
193
193
|
# fmt: on
|
|
194
194
|
# pylint: enable=line-too-long
|
|
@@ -579,7 +579,7 @@ class OobContext:
|
|
|
579
579
|
self.r = crypto.r() if r is None else r
|
|
580
580
|
|
|
581
581
|
def share(self) -> OobSharedData:
|
|
582
|
-
pkx =
|
|
582
|
+
pkx = self.ecc_key.x[::-1]
|
|
583
583
|
return OobSharedData(c=crypto.f4(pkx, pkx, self.r, bytes(1)), r=self.r)
|
|
584
584
|
|
|
585
585
|
|
|
@@ -677,6 +677,13 @@ class Session:
|
|
|
677
677
|
},
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
+
ea: bytes
|
|
681
|
+
eb: bytes
|
|
682
|
+
ltk: bytes
|
|
683
|
+
preq: bytes
|
|
684
|
+
pres: bytes
|
|
685
|
+
tk: bytes
|
|
686
|
+
|
|
680
687
|
def __init__(
|
|
681
688
|
self,
|
|
682
689
|
manager: Manager,
|
|
@@ -686,15 +693,10 @@ class Session:
|
|
|
686
693
|
) -> None:
|
|
687
694
|
self.manager = manager
|
|
688
695
|
self.connection = connection
|
|
689
|
-
self.preq: Optional[bytes] = None
|
|
690
|
-
self.pres: Optional[bytes] = None
|
|
691
|
-
self.ea = None
|
|
692
|
-
self.eb = None
|
|
693
696
|
self.stk = None
|
|
694
|
-
self.ltk = None
|
|
695
697
|
self.ltk_ediv = 0
|
|
696
698
|
self.ltk_rand = bytes(8)
|
|
697
|
-
self.link_key = None
|
|
699
|
+
self.link_key: Optional[bytes] = None
|
|
698
700
|
self.initiator_key_distribution: int = 0
|
|
699
701
|
self.responder_key_distribution: int = 0
|
|
700
702
|
self.peer_random_value: Optional[bytes] = None
|
|
@@ -787,9 +789,7 @@ class Session:
|
|
|
787
789
|
)
|
|
788
790
|
self.r = pairing_config.oob.our_context.r
|
|
789
791
|
self.ecc_key = pairing_config.oob.our_context.ecc_key
|
|
790
|
-
if pairing_config.oob.legacy_context is None:
|
|
791
|
-
self.tk = None
|
|
792
|
-
else:
|
|
792
|
+
if pairing_config.oob.legacy_context is not None:
|
|
793
793
|
self.tk = pairing_config.oob.legacy_context.tk
|
|
794
794
|
else:
|
|
795
795
|
if pairing_config.oob.legacy_context is None:
|
|
@@ -807,7 +807,7 @@ class Session:
|
|
|
807
807
|
|
|
808
808
|
@property
|
|
809
809
|
def pkx(self) -> Tuple[bytes, bytes]:
|
|
810
|
-
return (
|
|
810
|
+
return (self.ecc_key.x[::-1], self.peer_public_key_x)
|
|
811
811
|
|
|
812
812
|
@property
|
|
813
813
|
def pka(self) -> bytes:
|
|
@@ -1061,8 +1061,8 @@ class Session:
|
|
|
1061
1061
|
def send_public_key_command(self) -> None:
|
|
1062
1062
|
self.send_command(
|
|
1063
1063
|
SMP_Pairing_Public_Key_Command(
|
|
1064
|
-
public_key_x=
|
|
1065
|
-
public_key_y=
|
|
1064
|
+
public_key_x=self.ecc_key.x[::-1],
|
|
1065
|
+
public_key_y=self.ecc_key.y[::-1],
|
|
1066
1066
|
)
|
|
1067
1067
|
)
|
|
1068
1068
|
|
|
@@ -1098,15 +1098,52 @@ class Session:
|
|
|
1098
1098
|
)
|
|
1099
1099
|
)
|
|
1100
1100
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1101
|
+
@classmethod
|
|
1102
|
+
def derive_ltk(cls, link_key: bytes, ct2: bool) -> bytes:
|
|
1103
|
+
'''Derives Long Term Key from Link Key.
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
link_key: BR/EDR Link Key bytes in little-endian.
|
|
1107
|
+
ct2: whether ct2 is supported on both devices.
|
|
1108
|
+
Returns:
|
|
1109
|
+
LE Long Tern Key bytes in little-endian.
|
|
1110
|
+
'''
|
|
1104
1111
|
ilk = (
|
|
1105
1112
|
crypto.h7(salt=SMP_CTKD_H7_BRLE_SALT, w=link_key)
|
|
1106
|
-
if
|
|
1113
|
+
if ct2
|
|
1107
1114
|
else crypto.h6(link_key, b'tmp2')
|
|
1108
1115
|
)
|
|
1109
|
-
|
|
1116
|
+
return crypto.h6(ilk, b'brle')
|
|
1117
|
+
|
|
1118
|
+
@classmethod
|
|
1119
|
+
def derive_link_key(cls, ltk: bytes, ct2: bool) -> bytes:
|
|
1120
|
+
'''Derives Link Key from Long Term Key.
|
|
1121
|
+
|
|
1122
|
+
Args:
|
|
1123
|
+
ltk: LE Long Term Key bytes in little-endian.
|
|
1124
|
+
ct2: whether ct2 is supported on both devices.
|
|
1125
|
+
Returns:
|
|
1126
|
+
BR/EDR Link Key bytes in little-endian.
|
|
1127
|
+
'''
|
|
1128
|
+
ilk = (
|
|
1129
|
+
crypto.h7(salt=SMP_CTKD_H7_LEBR_SALT, w=ltk)
|
|
1130
|
+
if ct2
|
|
1131
|
+
else crypto.h6(ltk, b'tmp1')
|
|
1132
|
+
)
|
|
1133
|
+
return crypto.h6(ilk, b'lebr')
|
|
1134
|
+
|
|
1135
|
+
async def get_link_key_and_derive_ltk(self) -> None:
|
|
1136
|
+
'''Retrieves BR/EDR Link Key from storage and derive it to LE LTK.'''
|
|
1137
|
+
link_key = await self.manager.device.get_link_key(self.connection.peer_address)
|
|
1138
|
+
if link_key is None:
|
|
1139
|
+
logging.warning(
|
|
1140
|
+
'Try to derive LTK but host does not have the LK. Send a SMP_PAIRING_FAILED but the procedure will not be paused!'
|
|
1141
|
+
)
|
|
1142
|
+
self.send_pairing_failed(
|
|
1143
|
+
SMP_CROSS_TRANSPORT_KEY_DERIVATION_NOT_ALLOWED_ERROR
|
|
1144
|
+
)
|
|
1145
|
+
else:
|
|
1146
|
+
self.ltk = self.derive_ltk(link_key, self.ct2)
|
|
1110
1147
|
|
|
1111
1148
|
def distribute_keys(self) -> None:
|
|
1112
1149
|
# Distribute the keys as required
|
|
@@ -1117,7 +1154,7 @@ class Session:
|
|
|
1117
1154
|
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
|
1118
1155
|
):
|
|
1119
1156
|
self.ctkd_task = self.connection.abort_on(
|
|
1120
|
-
'disconnection', self.
|
|
1157
|
+
'disconnection', self.get_link_key_and_derive_ltk()
|
|
1121
1158
|
)
|
|
1122
1159
|
elif not self.sc:
|
|
1123
1160
|
# Distribute the LTK, EDIV and RAND
|
|
@@ -1147,12 +1184,7 @@ class Session:
|
|
|
1147
1184
|
|
|
1148
1185
|
# CTKD, calculate BR/EDR link key
|
|
1149
1186
|
if self.initiator_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG:
|
|
1150
|
-
|
|
1151
|
-
crypto.h7(salt=SMP_CTKD_H7_LEBR_SALT, w=self.ltk)
|
|
1152
|
-
if self.ct2
|
|
1153
|
-
else crypto.h6(self.ltk, b'tmp1')
|
|
1154
|
-
)
|
|
1155
|
-
self.link_key = crypto.h6(ilk, b'lebr')
|
|
1187
|
+
self.link_key = self.derive_link_key(self.ltk, self.ct2)
|
|
1156
1188
|
|
|
1157
1189
|
else:
|
|
1158
1190
|
# CTKD: Derive LTK from LinkKey
|
|
@@ -1161,7 +1193,7 @@ class Session:
|
|
|
1161
1193
|
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
|
1162
1194
|
):
|
|
1163
1195
|
self.ctkd_task = self.connection.abort_on(
|
|
1164
|
-
'disconnection', self.
|
|
1196
|
+
'disconnection', self.get_link_key_and_derive_ltk()
|
|
1165
1197
|
)
|
|
1166
1198
|
# Distribute the LTK, EDIV and RAND
|
|
1167
1199
|
elif not self.sc:
|
|
@@ -1191,12 +1223,7 @@ class Session:
|
|
|
1191
1223
|
|
|
1192
1224
|
# CTKD, calculate BR/EDR link key
|
|
1193
1225
|
if self.responder_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG:
|
|
1194
|
-
|
|
1195
|
-
crypto.h7(salt=SMP_CTKD_H7_LEBR_SALT, w=self.ltk)
|
|
1196
|
-
if self.ct2
|
|
1197
|
-
else crypto.h6(self.ltk, b'tmp1')
|
|
1198
|
-
)
|
|
1199
|
-
self.link_key = crypto.h6(ilk, b'lebr')
|
|
1226
|
+
self.link_key = self.derive_link_key(self.ltk, self.ct2)
|
|
1200
1227
|
|
|
1201
1228
|
def compute_peer_expected_distributions(self, key_distribution_flags: int) -> None:
|
|
1202
1229
|
# Set our expectations for what to wait for in the key distribution phase
|
|
@@ -1754,14 +1781,10 @@ class Session:
|
|
|
1754
1781
|
self.peer_public_key_y = command.public_key_y
|
|
1755
1782
|
|
|
1756
1783
|
# Compute the DH key
|
|
1757
|
-
self.dh_key =
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
bytes(reversed(command.public_key_y)),
|
|
1762
|
-
)
|
|
1763
|
-
)
|
|
1764
|
-
)
|
|
1784
|
+
self.dh_key = self.ecc_key.dh(
|
|
1785
|
+
command.public_key_x[::-1],
|
|
1786
|
+
command.public_key_y[::-1],
|
|
1787
|
+
)[::-1]
|
|
1765
1788
|
logger.debug(f'DH key: {self.dh_key.hex()}')
|
|
1766
1789
|
|
|
1767
1790
|
if self.pairing_method == PairingMethod.OOB:
|
|
@@ -1824,7 +1847,6 @@ class Session:
|
|
|
1824
1847
|
else:
|
|
1825
1848
|
self.send_pairing_dhkey_check_command()
|
|
1826
1849
|
else:
|
|
1827
|
-
assert self.ltk
|
|
1828
1850
|
self.start_encryption(self.ltk)
|
|
1829
1851
|
|
|
1830
1852
|
def on_smp_pairing_failed_command(
|
|
@@ -1874,6 +1896,7 @@ class Manager(EventEmitter):
|
|
|
1874
1896
|
sessions: Dict[int, Session]
|
|
1875
1897
|
pairing_config_factory: Callable[[Connection], PairingConfig]
|
|
1876
1898
|
session_proxy: Type[Session]
|
|
1899
|
+
_ecc_key: Optional[crypto.EccKey]
|
|
1877
1900
|
|
|
1878
1901
|
def __init__(
|
|
1879
1902
|
self,
|