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/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 .colors import color
21
- from .att import ATT_CID, ATT_PDU
22
- from .smp import SMP_CID, SMP_Command
23
- from .core import name_or_number
24
- from .l2cap import (
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
- def __init__(self, analyzer):
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
- self.psms[control_frame.source_cid] = control_frame.psm
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
- control_frame.result
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(control_frame.source_cid):
104
+ if psm := self.peer.psms.get(
105
+ connection_response.source_cid
106
+ ):
92
107
  # Found a pending connection
93
- self.psms[control_frame.destination_cid] = psm
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
- control_frame.source_cid
100
- ] = AVDTP_MessageAssembler(self.on_avdtp_message)
114
+ connection_response.source_cid
115
+ ] = avdtp.MessageAssembler(self.on_avdtp_message)
101
116
  self.peer.avdtp_assemblers[
102
- control_frame.destination_cid
103
- ] = AVDTP_MessageAssembler(
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(self, transaction_label, 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
- def __init__(self, label, emit_message):
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 (stream := self.acl_streams.get(packet.connection_handle)) is None:
181
- stream = self.start_acl_stream(packet.connection_handle)
182
- stream.feed_packet(packet)
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
- if packet.event_code == HCI_DISCONNECTION_COMPLETE_EVENT:
185
- self.end_acl_stream(packet.connection_handle)
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, Tuple, Callable, Dict, Union, TYPE_CHECKING
24
+ from typing import Optional, TYPE_CHECKING
26
25
 
27
- from . import core, l2cap # type: ignore
28
- from .colors import color # type: ignore
29
- from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError # type: ignore
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.send_pdu(msg) # type: ignore
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.send_pdu(msg) # type: ignore
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
@@ -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, device: Device, connection: Connection) -> None:
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 mutliplexer to manage DLCs with the server
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, device: Device) -> None:
764
- self.device = device
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, connection: Connection) -> None:
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('00000000000000000000000000000000746D7031')
191
- SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032')
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 = bytes(reversed(self.ecc_key.x))
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 (bytes(reversed(self.ecc_key.x)), self.peer_public_key_x)
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=bytes(reversed(self.ecc_key.x)),
1065
- public_key_y=bytes(reversed(self.ecc_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
- async def derive_ltk(self) -> None:
1102
- link_key = await self.manager.device.get_link_key(self.connection.peer_address)
1103
- assert link_key is not None
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 self.ct2
1113
+ if ct2
1107
1114
  else crypto.h6(link_key, b'tmp2')
1108
1115
  )
1109
- self.ltk = crypto.h6(ilk, b'brle')
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.derive_ltk()
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
- ilk = (
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.derive_ltk()
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
- ilk = (
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 = bytes(
1758
- reversed(
1759
- self.ecc_key.dh(
1760
- bytes(reversed(command.public_key_x)),
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,