diff --git a/.vscode/settings.json b/.vscode/settings.json index 04a7f409..466158f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,7 @@ "deregistration", "dhkey", "diversifier", + "endianness", "Fitbit", "GATTLINK", "HANDSFREE", diff --git a/bumble/crypto.py b/bumble/crypto.py index 852c675a..d267c3b7 100644 --- a/bumble/crypto.py +++ b/bumble/crypto.py @@ -21,6 +21,8 @@ # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- +from __future__ import annotations + import logging import operator @@ -29,11 +31,13 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.asymmetric.ec import ( generate_private_key, ECDH, + EllipticCurvePrivateKey, EllipticCurvePublicNumbers, EllipticCurvePrivateNumbers, SECP256R1, ) from cryptography.hazmat.primitives import cmac +from typing import Tuple # ----------------------------------------------------------------------------- @@ -46,16 +50,18 @@ logger = logging.getLogger(__name__) # Classes # ----------------------------------------------------------------------------- class EccKey: - def __init__(self, private_key): + def __init__(self, private_key: EllipticCurvePrivateKey) -> None: self.private_key = private_key @classmethod - def generate(cls): + def generate(cls) -> EccKey: private_key = generate_private_key(SECP256R1()) return cls(private_key) @classmethod - def from_private_key_bytes(cls, d_bytes, x_bytes, y_bytes): + def from_private_key_bytes( + cls, d_bytes: bytes, x_bytes: bytes, y_bytes: bytes + ) -> EccKey: d = int.from_bytes(d_bytes, byteorder='big', signed=False) x = int.from_bytes(x_bytes, byteorder='big', signed=False) y = int.from_bytes(y_bytes, byteorder='big', signed=False) @@ -65,7 +71,7 @@ class EccKey: return cls(private_key) @property - def x(self): + def x(self) -> bytes: return ( self.private_key.public_key() .public_numbers() @@ -73,14 +79,14 @@ class EccKey: ) @property - def y(self): + def y(self) -> bytes: return ( self.private_key.public_key() .public_numbers() .y.to_bytes(32, byteorder='big') ) - def dh(self, public_key_x, public_key_y): + def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes: x = int.from_bytes(public_key_x, byteorder='big', signed=False) y = int.from_bytes(public_key_y, byteorder='big', signed=False) public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key() @@ -93,14 +99,23 @@ class EccKey: # Functions # ----------------------------------------------------------------------------- + # ----------------------------------------------------------------------------- -def xor(x, y): +def xor(x: bytes, y: bytes) -> bytes: assert len(x) == len(y) return bytes(map(operator.xor, x, y)) # ----------------------------------------------------------------------------- -def r(): +def reverse(input: bytes) -> bytes: + ''' + Returns bytes of input in reversed endianness. + ''' + return input[::-1] + + +# ----------------------------------------------------------------------------- +def r() -> bytes: ''' Generate 16 bytes of random data ''' @@ -108,20 +123,20 @@ def r(): # ----------------------------------------------------------------------------- -def e(key, data): +def e(key: bytes, data: bytes) -> bytes: ''' AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output. See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e ''' - cipher = Cipher(algorithms.AES(bytes(reversed(key))), modes.ECB()) + cipher = Cipher(algorithms.AES(reverse(key)), modes.ECB()) encryptor = cipher.encryptor() - return bytes(reversed(encryptor.update(bytes(reversed(data))))) + return reverse(encryptor.update(reverse(data))) # ----------------------------------------------------------------------------- -def ah(k, r): # pylint: disable=redefined-outer-name +def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name ''' See Bluetooth spec Vol 3, Part H - 2.2.2 Random Address Hash function ah ''' @@ -132,7 +147,16 @@ def ah(k, r): # pylint: disable=redefined-outer-name # ----------------------------------------------------------------------------- -def c1(k, r, preq, pres, iat, rat, ia, ra): # pylint: disable=redefined-outer-name +def c1( + k: bytes, + r: bytes, + preq: bytes, + pres: bytes, + iat: int, + rat: int, + ia: bytes, + ra: bytes, +) -> bytes: # pylint: disable=redefined-outer-name ''' See Bluetooth spec, Vol 3, Part H - 2.2.3 Confirm value generation function c1 for LE Legacy Pairing @@ -144,7 +168,7 @@ def c1(k, r, preq, pres, iat, rat, ia, ra): # pylint: disable=redefined-outer-n # ----------------------------------------------------------------------------- -def s1(k, r1, r2): +def s1(k: bytes, r1: bytes, r2: bytes) -> bytes: ''' See Bluetooth spec, Vol 3, Part H - 2.2.4 Key generation function s1 for LE Legacy Pairing @@ -154,7 +178,7 @@ def s1(k, r1, r2): # ----------------------------------------------------------------------------- -def aes_cmac(m, k): +def aes_cmac(m: bytes, k: bytes) -> bytes: ''' See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC @@ -166,20 +190,16 @@ def aes_cmac(m, k): # ----------------------------------------------------------------------------- -def f4(u, v, x, z): +def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes: ''' See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value Generation Function f4 ''' - return bytes( - reversed( - aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + z, bytes(reversed(x))) - ) - ) + return reverse(aes_cmac(reverse(u) + reverse(v) + z, reverse(x))) # ----------------------------------------------------------------------------- -def f5(w, n1, n2, a1, a2): +def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> Tuple[bytes, bytes]: ''' See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation Function f5 @@ -187,87 +207,83 @@ def f5(w, n1, n2, a1, a2): NOTE: this returns a tuple: (MacKey, LTK) in little-endian byte order ''' salt = bytes.fromhex('6C888391AAF5A53860370BDB5A6083BE') - t = aes_cmac(bytes(reversed(w)), salt) + t = aes_cmac(reverse(w), salt) key_id = bytes([0x62, 0x74, 0x6C, 0x65]) return ( - bytes( - reversed( - aes_cmac( - bytes([0]) - + key_id - + bytes(reversed(n1)) - + bytes(reversed(n2)) - + bytes(reversed(a1)) - + bytes(reversed(a2)) - + bytes([1, 0]), - t, - ) + reverse( + aes_cmac( + bytes([0]) + + key_id + + reverse(n1) + + reverse(n2) + + reverse(a1) + + reverse(a2) + + bytes([1, 0]), + t, ) ), - bytes( - reversed( - aes_cmac( - bytes([1]) - + key_id - + bytes(reversed(n1)) - + bytes(reversed(n2)) - + bytes(reversed(a1)) - + bytes(reversed(a2)) - + bytes([1, 0]), - t, - ) + reverse( + aes_cmac( + bytes([1]) + + key_id + + reverse(n1) + + reverse(n2) + + reverse(a1) + + reverse(a2) + + bytes([1, 0]), + t, ) ), ) # ----------------------------------------------------------------------------- -def f6(w, n1, n2, r, io_cap, a1, a2): # pylint: disable=redefined-outer-name +def f6( + w: bytes, n1: bytes, n2: bytes, r: bytes, io_cap: bytes, a1: bytes, a2: bytes +) -> bytes: # pylint: disable=redefined-outer-name ''' See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value Generation Function f6 ''' - return bytes( - reversed( - aes_cmac( - bytes(reversed(n1)) - + bytes(reversed(n2)) - + bytes(reversed(r)) - + bytes(reversed(io_cap)) - + bytes(reversed(a1)) - + bytes(reversed(a2)), - bytes(reversed(w)), - ) + return reverse( + aes_cmac( + reverse(n1) + + reverse(n2) + + reverse(r) + + reverse(io_cap) + + reverse(a1) + + reverse(a2), + reverse(w), ) ) # ----------------------------------------------------------------------------- -def g2(u, v, x, y): +def g2(u: bytes, v: bytes, x: bytes, y: bytes) -> int: ''' See Bluetooth spec, Vol 3, Part H - 2.2.9 LE Secure Connections Numeric Comparison Value Generation Function g2 ''' return int.from_bytes( aes_cmac( - bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), - bytes(reversed(x)), + reverse(u) + reverse(v) + reverse(y), + reverse(x), )[-4:], byteorder='big', ) # ----------------------------------------------------------------------------- -def h6(w, key_id): +def h6(w: bytes, key_id: bytes) -> bytes: ''' See Bluetooth spec, Vol 3, Part H - 2.2.10 Link key conversion function h6 ''' - return aes_cmac(key_id, w) + return reverse(aes_cmac(key_id, reverse(w))) # ----------------------------------------------------------------------------- -def h7(salt, w): +def h7(salt: bytes, w: bytes) -> bytes: ''' See Bluetooth spec, Vol 3, Part H - 2.2.11 Link key conversion function h7 ''' - return aes_cmac(w, salt) + return reverse(aes_cmac(reverse(w), salt)) diff --git a/bumble/smp.py b/bumble/smp.py index 1461969c..25dd46b5 100644 --- a/bumble/smp.py +++ b/bumble/smp.py @@ -187,8 +187,8 @@ SMP_KEYPRESS_AUTHREQ = 0b00010000 SMP_CT2_AUTHREQ = 0b00100000 # Crypto salt -SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('00000000000000000000000000000000746D7031') -SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('00000000000000000000000000000000746D7032') +SMP_CTKD_H7_LEBR_SALT = bytes.fromhex('000000000000000000000000746D7031') +SMP_CTKD_H7_BRLE_SALT = bytes.fromhex('000000000000000000000000746D7032') # fmt: on # pylint: enable=line-too-long @@ -579,7 +579,7 @@ class OobContext: self.r = crypto.r() if r is None else r def share(self) -> OobSharedData: - pkx = bytes(reversed(self.ecc_key.x)) + pkx = self.ecc_key.x[::-1] return OobSharedData(c=crypto.f4(pkx, pkx, self.r, bytes(1)), r=self.r) @@ -677,6 +677,13 @@ class Session: }, } + ea: bytes + eb: bytes + ltk: bytes + preq: bytes + pres: bytes + tk: bytes + def __init__( self, manager: Manager, @@ -686,15 +693,10 @@ class Session: ) -> None: self.manager = manager self.connection = connection - self.preq: Optional[bytes] = None - self.pres: Optional[bytes] = None - self.ea = None - self.eb = None self.stk = None - self.ltk = None self.ltk_ediv = 0 self.ltk_rand = bytes(8) - self.link_key = None + self.link_key: Optional[bytes] = None self.initiator_key_distribution: int = 0 self.responder_key_distribution: int = 0 self.peer_random_value: Optional[bytes] = None @@ -787,9 +789,7 @@ class Session: ) self.r = pairing_config.oob.our_context.r self.ecc_key = pairing_config.oob.our_context.ecc_key - if pairing_config.oob.legacy_context is None: - self.tk = None - else: + if pairing_config.oob.legacy_context is not None: self.tk = pairing_config.oob.legacy_context.tk else: if pairing_config.oob.legacy_context is None: @@ -807,7 +807,7 @@ class Session: @property def pkx(self) -> Tuple[bytes, bytes]: - return (bytes(reversed(self.ecc_key.x)), self.peer_public_key_x) + return (self.ecc_key.x[::-1], self.peer_public_key_x) @property def pka(self) -> bytes: @@ -1061,8 +1061,8 @@ class Session: def send_public_key_command(self) -> None: self.send_command( SMP_Pairing_Public_Key_Command( - public_key_x=bytes(reversed(self.ecc_key.x)), - public_key_y=bytes(reversed(self.ecc_key.y)), + public_key_x=self.ecc_key.x[::-1], + public_key_y=self.ecc_key.y[::-1], ) ) @@ -1098,15 +1098,52 @@ class Session: ) ) - async def derive_ltk(self) -> None: - link_key = await self.manager.device.get_link_key(self.connection.peer_address) - assert link_key is not None + @classmethod + def derive_ltk(cls, link_key: bytes, ct2: bool) -> bytes: + '''Derives Long Term Key from Link Key. + + Args: + link_key: BR/EDR Link Key bytes in little-endian. + ct2: whether ct2 is supported on both devices. + Returns: + LE Long Tern Key bytes in little-endian. + ''' ilk = ( crypto.h7(salt=SMP_CTKD_H7_BRLE_SALT, w=link_key) - if self.ct2 + if ct2 else crypto.h6(link_key, b'tmp2') ) - self.ltk = crypto.h6(ilk, b'brle') + return crypto.h6(ilk, b'brle') + + @classmethod + def derive_link_key(cls, ltk: bytes, ct2: bool) -> bytes: + '''Derives Link Key from Long Term Key. + + Args: + ltk: LE Long Term Key bytes in little-endian. + ct2: whether ct2 is supported on both devices. + Returns: + BR/EDR Link Key bytes in little-endian. + ''' + ilk = ( + crypto.h7(salt=SMP_CTKD_H7_LEBR_SALT, w=ltk) + if ct2 + else crypto.h6(ltk, b'tmp1') + ) + return crypto.h6(ilk, b'lebr') + + async def get_link_key_and_derive_ltk(self) -> None: + '''Retrieves BR/EDR Link Key from storage and derive it to LE LTK.''' + link_key = await self.manager.device.get_link_key(self.connection.peer_address) + if link_key is None: + logging.warning( + 'Try to derive LTK but host does not have the LK. Send a SMP_PAIRING_FAILED but the procedure will not be paused!' + ) + self.send_pairing_failed( + SMP_CROSS_TRANSPORT_KEY_DERIVATION_NOT_ALLOWED_ERROR + ) + else: + self.ltk = self.derive_ltk(link_key, self.ct2) def distribute_keys(self) -> None: # Distribute the keys as required @@ -1117,7 +1154,7 @@ class Session: and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG ): self.ctkd_task = self.connection.abort_on( - 'disconnection', self.derive_ltk() + 'disconnection', self.get_link_key_and_derive_ltk() ) elif not self.sc: # Distribute the LTK, EDIV and RAND @@ -1147,12 +1184,7 @@ class Session: # CTKD, calculate BR/EDR link key if self.initiator_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG: - ilk = ( - crypto.h7(salt=SMP_CTKD_H7_LEBR_SALT, w=self.ltk) - if self.ct2 - else crypto.h6(self.ltk, b'tmp1') - ) - self.link_key = crypto.h6(ilk, b'lebr') + self.link_key = self.derive_link_key(self.ltk, self.ct2) else: # CTKD: Derive LTK from LinkKey @@ -1161,7 +1193,7 @@ class Session: and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG ): self.ctkd_task = self.connection.abort_on( - 'disconnection', self.derive_ltk() + 'disconnection', self.get_link_key_and_derive_ltk() ) # Distribute the LTK, EDIV and RAND elif not self.sc: @@ -1191,12 +1223,7 @@ class Session: # CTKD, calculate BR/EDR link key if self.responder_key_distribution & SMP_LINK_KEY_DISTRIBUTION_FLAG: - ilk = ( - crypto.h7(salt=SMP_CTKD_H7_LEBR_SALT, w=self.ltk) - if self.ct2 - else crypto.h6(self.ltk, b'tmp1') - ) - self.link_key = crypto.h6(ilk, b'lebr') + self.link_key = self.derive_link_key(self.ltk, self.ct2) def compute_peer_expected_distributions(self, key_distribution_flags: int) -> None: # Set our expectations for what to wait for in the key distribution phase @@ -1754,14 +1781,10 @@ class Session: self.peer_public_key_y = command.public_key_y # Compute the DH key - self.dh_key = bytes( - reversed( - self.ecc_key.dh( - bytes(reversed(command.public_key_x)), - bytes(reversed(command.public_key_y)), - ) - ) - ) + self.dh_key = self.ecc_key.dh( + command.public_key_x[::-1], + command.public_key_y[::-1], + )[::-1] logger.debug(f'DH key: {self.dh_key.hex()}') if self.pairing_method == PairingMethod.OOB: @@ -1824,7 +1847,6 @@ class Session: else: self.send_pairing_dhkey_check_command() else: - assert self.ltk self.start_encryption(self.ltk) def on_smp_pairing_failed_command( @@ -1874,6 +1896,7 @@ class Manager(EventEmitter): sessions: Dict[int, Session] pairing_config_factory: Callable[[Connection], PairingConfig] session_proxy: Type[Session] + _ecc_key: Optional[crypto.EccKey] def __init__( self, diff --git a/tests/self_test.py b/tests/self_test.py index 728fbc7e..7a144870 100644 --- a/tests/self_test.py +++ b/tests/self_test.py @@ -21,7 +21,7 @@ import logging import os import pytest -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from bumble.controller import Controller from bumble.core import BT_BR_EDR_TRANSPORT, BT_PERIPHERAL_ROLE, BT_CENTRAL_ROLE @@ -38,7 +38,6 @@ from bumble.smp import ( OobLegacyContext, ) from bumble.core import ProtocolError -from bumble.hci import HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE from bumble.keys import PairingKeys @@ -519,16 +518,8 @@ async def test_self_smp_over_classic(): # Mock connection # TODO: Implement Classic SSP and encryption in link relayer LINK_KEY = bytes.fromhex('287ad379dca402530a39f1f43047b835') - two_devices.devices[0].on_link_key( - two_devices.devices[1].public_address, - LINK_KEY, - HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE, - ) - two_devices.devices[1].on_link_key( - two_devices.devices[0].public_address, - LINK_KEY, - HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE, - ) + two_devices.devices[0].get_link_key = AsyncMock(return_value=LINK_KEY) + two_devices.devices[1].get_link_key = AsyncMock(return_value=LINK_KEY) two_devices.connections[0].encryption = 1 two_devices.connections[1].encryption = 1 diff --git a/tests/smp_test.py b/tests/smp_test.py index 4bd75228..7a32b23c 100644 --- a/tests/smp_test.py +++ b/tests/smp_test.py @@ -16,6 +16,9 @@ # Imports # ----------------------------------------------------------------------------- +import pytest + +from bumble import smp from bumble.crypto import EccKey, aes_cmac, ah, c1, f4, f5, f6, g2, h6, h7, s1 from bumble.pairing import OobData, OobSharedData, LeRole from bumble.hci import Address @@ -28,8 +31,8 @@ from bumble.core import AdvertisingData # ----------------------------------------------------------------------------- -def reversed_hex(hex_str): - return bytes(reversed(bytes.fromhex(hex_str))) +def reversed_hex(hex_str: str) -> bytes: + return bytes.fromhex(hex_str)[::-1] # ----------------------------------------------------------------------------- @@ -129,112 +132,79 @@ def test_aes_cmac(): # ----------------------------------------------------------------------------- def test_f4(): - u = bytes( - reversed( - bytes.fromhex( - '20b003d2 f297be2c 5e2c83a7 e9f9a5b9' - + 'eff49111 acf4fddb cc030148 0e359de6' - ) - ) + u = reversed_hex( + '20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6' ) - v = bytes( - reversed( - bytes.fromhex( - '55188b3d 32f6bb9a 900afcfb eed4e72a' - + '59cb9ac2 f19d7cfb 6b4fdd49 f47fc5fd' - ) - ) + v = reversed_hex( + '55188b3d 32f6bb9a 900afcfb eed4e72a 59cb9ac2 f19d7cfb 6b4fdd49 f47fc5fd' ) - x = bytes(reversed(bytes.fromhex('d5cb8454 d177733e ffffb2ec 712baeab'))) - z = bytes([0]) + x = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab') + z = b'\0' value = f4(u, v, x, z) - assert bytes(reversed(value)) == bytes.fromhex( - 'f2c916f1 07a9bd1c f1eda1be a974872d' - ) + assert value == reversed_hex('f2c916f1 07a9bd1c f1eda1be a974872d') # ----------------------------------------------------------------------------- def test_f5(): - w = bytes( - reversed( - bytes.fromhex( - 'ec0234a3 57c8ad05 341010a6 0a397d9b' - + '99796b13 b4f866f1 868d34f3 73bfa698' - ) - ) + w = reversed_hex( + 'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698' ) - n1 = bytes(reversed(bytes.fromhex('d5cb8454 d177733e ffffb2ec 712baeab'))) - n2 = bytes(reversed(bytes.fromhex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf'))) - a1 = bytes(reversed(bytes.fromhex('00561237 37bfce'))) - a2 = bytes(reversed(bytes.fromhex('00a71370 2dcfc1'))) + n1 = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab') + n2 = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf') + a1 = reversed_hex('00561237 37bfce') + a2 = reversed_hex('00a71370 2dcfc1') value = f5(w, n1, n2, a1, a2) - assert bytes(reversed(value[0])) == bytes.fromhex( - '2965f176 a1084a02 fd3f6a20 ce636e20' - ) - assert bytes(reversed(value[1])) == bytes.fromhex( - '69867911 69d7cd23 980522b5 94750a38' - ) + assert value[0] == reversed_hex('2965f176 a1084a02 fd3f6a20 ce636e20') + assert value[1] == reversed_hex('69867911 69d7cd23 980522b5 94750a38') # ----------------------------------------------------------------------------- def test_f6(): - n1 = bytes(reversed(bytes.fromhex('d5cb8454 d177733e ffffb2ec 712baeab'))) - n2 = bytes(reversed(bytes.fromhex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf'))) - mac_key = bytes(reversed(bytes.fromhex('2965f176 a1084a02 fd3f6a20 ce636e20'))) - r = bytes(reversed(bytes.fromhex('12a3343b b453bb54 08da42d2 0c2d0fc8'))) - io_cap = bytes(reversed(bytes.fromhex('010102'))) - a1 = bytes(reversed(bytes.fromhex('00561237 37bfce'))) - a2 = bytes(reversed(bytes.fromhex('00a71370 2dcfc1'))) + n1 = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab') + n2 = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf') + mac_key = reversed_hex('2965f176 a1084a02 fd3f6a20 ce636e20') + r = reversed_hex('12a3343b b453bb54 08da42d2 0c2d0fc8') + io_cap = reversed_hex('010102') + a1 = reversed_hex('00561237 37bfce') + a2 = reversed_hex('00a71370 2dcfc1') value = f6(mac_key, n1, n2, r, io_cap, a1, a2) - assert bytes(reversed(value)) == bytes.fromhex( - 'e3c47398 9cd0e8c5 d26c0b09 da958f61' - ) + assert value == reversed_hex('e3c47398 9cd0e8c5 d26c0b09 da958f61') # ----------------------------------------------------------------------------- def test_g2(): - u = bytes( - reversed( - bytes.fromhex( - '20b003d2 f297be2c 5e2c83a7 e9f9a5b9' - + 'eff49111 acf4fddb cc030148 0e359de6' - ) - ) + u = reversed_hex( + '20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6' ) - v = bytes( - reversed( - bytes.fromhex( - '55188b3d 32f6bb9a 900afcfb eed4e72a' - + '59cb9ac2 f19d7cfb 6b4fdd49 f47fc5fd' - ) - ) + v = reversed_hex( + '55188b3d 32f6bb9a 900afcfb eed4e72a 59cb9ac2 f19d7cfb 6b4fdd49 f47fc5fd' ) - x = bytes(reversed(bytes.fromhex('d5cb8454 d177733e ffffb2ec 712baeab'))) - y = bytes(reversed(bytes.fromhex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf'))) + x = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab') + y = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf') value = g2(u, v, x, y) assert value == 0x2F9ED5BA # ----------------------------------------------------------------------------- def test_h6(): - KEY = bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b') + KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b') KEY_ID = bytes.fromhex('6c656272') - assert h6(KEY, KEY_ID) == bytes.fromhex('2d9ae102 e76dc91c e8d3a9e2 80b16399') + assert h6(KEY, KEY_ID) == reversed_hex('2d9ae102 e76dc91c e8d3a9e2 80b16399') # ----------------------------------------------------------------------------- def test_h7(): - KEY = bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b') + KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b') SALT = bytes.fromhex('00000000 00000000 00000000 746D7031') - assert h7(SALT, KEY) == bytes.fromhex('fb173597 c6a3c0ec d2998c2a 75a57011') + assert h7(SALT, KEY) == reversed_hex('fb173597 c6a3c0ec d2998c2a 75a57011') # ----------------------------------------------------------------------------- def test_ah(): - irk = bytes(reversed(bytes.fromhex('ec0234a3 57c8ad05 341010a6 0a397d9b'))) - prand = bytes(reversed(bytes.fromhex('708194'))) + irk = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b') + prand = reversed_hex('708194') value = ah(irk, prand) - expected = bytes(reversed(bytes.fromhex('0dfbaa'))) + expected = reversed_hex('0dfbaa') assert value == expected @@ -243,7 +213,7 @@ def test_oob_data(): oob_data = OobData( address=Address("F0:F1:F2:F3:F4:F5"), role=LeRole.BOTH_PERIPHERAL_PREFERRED, - shared_data=OobSharedData(c=bytes([1, 2]), r=bytes([3, 4])), + shared_data=OobSharedData(c=b'12', r=b'34'), ) oob_data_ad = oob_data.to_ad() oob_data_bytes = bytes(oob_data_ad) @@ -255,6 +225,32 @@ def test_oob_data(): assert oob_data_parsed.shared_data.r == oob_data.shared_data.r +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize( + 'ct2, expected', + [ + (False, 'bc1ca4ef 633fc1bd 0d8230af ee388fb0'), + (True, '287ad379 dca40253 0a39f1f4 3047b835'), + ], +) +def test_ltk_to_link_key(ct2: bool, expected: str): + LTK = reversed_hex('368df9bc e3264b58 bd066c33 334fbf64') + assert smp.Session.derive_link_key(LTK, ct2) == reversed_hex(expected) + + +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize( + 'ct2, expected', + [ + (False, 'a813fb72 f1a3dfa1 8a2c9a43 f10d0a30'), + (True, 'e85e09eb 5eccb3e2 69418a13 3211bc79'), + ], +) +def test_link_key_to_ltk(ct2: bool, expected: str): + LINK_KEY = reversed_hex('05040302 01000908 07060504 03020100') + assert smp.Session.derive_ltk(LINK_KEY, ct2) == reversed_hex(expected) + + # ----------------------------------------------------------------------------- if __name__ == '__main__': test_ecc()