From bc0e8911fea0b78bb4df8714519b66ad7fb10583 Mon Sep 17 00:00:00 2001 From: yhql Date: Tue, 24 May 2022 14:01:29 +0200 Subject: [PATCH 1/6] Add BLE support with 'bleak' --- ledgerwallet/transport/__init__.py | 3 +- ledgerwallet/transport/ble.py | 132 +++++++++++++++++++++++++++++ pyproject.toml | 1 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 ledgerwallet/transport/ble.py diff --git a/ledgerwallet/transport/__init__.py b/ledgerwallet/transport/__init__.py index 452cdf2..4ea567b 100644 --- a/ledgerwallet/transport/__init__.py +++ b/ledgerwallet/transport/__init__.py @@ -1,10 +1,11 @@ from contextlib import contextmanager from .device import Device +from .ble import BleDevice from .hid import HidDevice from .tcp import TcpDevice -DEVICE_CLASSES = [TcpDevice, HidDevice] +DEVICE_CLASSES = [TcpDevice, HidDevice, BleDevice] def enumerate_devices(): diff --git a/ledgerwallet/transport/ble.py b/ledgerwallet/transport/ble.py new file mode 100644 index 0000000..00de0eb --- /dev/null +++ b/ledgerwallet/transport/ble.py @@ -0,0 +1,132 @@ +import asyncio +from bleak import BleakClient, BleakScanner +from bleak.exc import BleakError +from typing import List + +HANDLE_CHAR_ENABLE_NOTIF = 13 +HANDLE_CHAR_WRITE = 16 +TAG_ID = b"\x05" + + +queue: asyncio.Queue = asyncio.Queue() + + +async def ble_discover(): + devices = await BleakScanner.discover(2) + return devices + + +def callback(sender, data): + response = bytes(data) + queue.put_nowait(response) + + +async def _get_client(ble_address: str) -> BleakClient: + device = await BleakScanner.find_device_by_address(ble_address, timeout=2.0) + if not device: + raise BleakError(f"Device with address {ble_address} could not be found.") + + client = BleakClient(device) + await client.connect() + + # register notification callback + # callback = lambda sender, data: queue.put_nowait(bytes(data)) + await client.start_notify(HANDLE_CHAR_ENABLE_NOTIF, callback) + + # enable notifications + await client.write_gatt_char(HANDLE_CHAR_WRITE, bytes.fromhex("0001"), True) + assert await queue.get() == b"\x00\x00\x00\x00\x00" + + # confirm that the MTU is 0x99 + await client.write_gatt_char(HANDLE_CHAR_WRITE, bytes.fromhex("0800000000"), True) + assert await queue.get() == b"\x08\x00\x00\x00\x01\x99" + + return client + + +async def _read() -> bytes: + response = await queue.get() + + assert len(response) >= 5 + assert response[0] == TAG_ID[0] + assert response[1:3] == b"\x00\x00" + total_size = int.from_bytes(response[3:5], "big") + + apdu = response[5:] + i = 1 + if len(apdu) < total_size: + assert total_size > len(response) - 5 + + response = await queue.get() + + assert len(response) >= 3 + assert response[0] == TAG_ID[0] + assert int.from_bytes(response[1:3], "big") == i + i += 1 + apdu += response[3:] + + assert len(apdu) == total_size + return apdu + + +async def _write(client: BleakClient, data: bytes, mtu: int = 0x99): + chunks: List[bytes] = [] + buffer = data + while buffer: + if not chunks: + size = 5 + else: + size = 3 + size = mtu - size + chunks.append(buffer[:size]) + buffer = buffer[size:] + + for i, chunk in enumerate(chunks): + header = TAG_ID + header += i.to_bytes(2, "big") + if i == 0: + header += len(data).to_bytes(2, "big") + await client.write_gatt_char(HANDLE_CHAR_WRITE, header + chunk, True) + + +class BleDevice(object): + def __init__(self, device): + self.device = device + self.loop = None + self.client = None + self.opened = False + + @classmethod + def enumerate_devices(cls): + loop = asyncio.get_event_loop() + discovered_devices = loop.run_until_complete(ble_discover()) + devices = [] + for device in discovered_devices: + if device.name is not None: + if device.name.startswith("Nano X"): + devices.append(BleDevice(device)) + return devices + + def __str__(self): + return "[BLE Device] {} ({})".format(self.device.name, self.device.address) + + def open(self): + self.loop = asyncio.get_event_loop() + self.client = self.loop.run_until_complete(_get_client(self.device.address)) + self.opened = True + + def close(self): + if self.opened: + self.loop.run_until_complete(self.client.disconnect()) + self.opened = False + self.loop.close() + + def write(self, data: bytes): + self.loop.run_until_complete(_write(self.client, data)) + + def read(self) -> bytes: + return self.loop.run_until_complete(_read()) + + def exchange(self, data: bytes, timeout=1000): + self.write(data) + return self.read() diff --git a/pyproject.toml b/pyproject.toml index 59459fa..f45ee3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ dynamic = ["version", "description"] requires-python = ">=3.7" dependencies = [ + "bleak", "click >=8.0", "construct >=2.10", "cryptography >=2.5", From 8cbec3e29e2f1669b8c312737e581d4099ea0f2d Mon Sep 17 00:00:00 2001 From: yhql Date: Tue, 24 May 2022 14:02:27 +0200 Subject: [PATCH 2/6] Show connected device in verbose mode --- ledgerwallet/client.py | 1 + ledgerwallet/transport/hid.py | 3 +++ ledgerwallet/transport/tcp.py | 3 +++ 3 files changed, 7 insertions(+) diff --git a/ledgerwallet/client.py b/ledgerwallet/client.py index 4106844..bc1fe0b 100644 --- a/ledgerwallet/client.py +++ b/ledgerwallet/client.py @@ -170,6 +170,7 @@ def __init__(self, device=None, cla=0xE0, private_key=None): raise NoLedgerDeviceException("No Ledger device has been found.") device = devices[0] self.device = device + LOG.debug(self.device) self.cla = cla self._target_id = None self.scp = None diff --git a/ledgerwallet/transport/hid.py b/ledgerwallet/transport/hid.py index e078c98..0428025 100644 --- a/ledgerwallet/transport/hid.py +++ b/ledgerwallet/transport/hid.py @@ -22,6 +22,9 @@ def enumerate_devices(cls): devices.append(HidDevice(hid_device_path)) return devices + def __str__(self): + return "[HID Device] {}".format(self.path.decode()) + def get_name(self): return "hid:{}".format(self.path.decode()) diff --git a/ledgerwallet/transport/tcp.py b/ledgerwallet/transport/tcp.py index ace9f93..1f4a19f 100644 --- a/ledgerwallet/transport/tcp.py +++ b/ledgerwallet/transport/tcp.py @@ -28,6 +28,9 @@ def enumerate_devices(cls): else: return [] + def __str__(self): + return "[TCP Device] {}:{}".format(self.server, self.port) + def open(self): self.socket.connect((self.server, self.port)) From b13e0767c42e8ae267b3a38f4e4d6c37196ff547 Mon Sep 17 00:00:00 2001 From: yhql Date: Mon, 30 May 2022 15:16:03 +0200 Subject: [PATCH 3/6] Close connections when LedgerClient is destroyed --- ledgerwallet/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ledgerwallet/client.py b/ledgerwallet/client.py index bc1fe0b..c8b6317 100644 --- a/ledgerwallet/client.py +++ b/ledgerwallet/client.py @@ -180,6 +180,9 @@ def __init__(self, device=None, cla=0xE0, private_key=None): self.private_key = PrivateKey(private_key) self.device.open() + def __del__(self): + self.close() + def close(self): self.device.close() From 3af78780b20c3f8b9dae9eb504d2f2d662b4477f Mon Sep 17 00:00:00 2001 From: yhql Date: Fri, 1 Jul 2022 10:33:31 +0200 Subject: [PATCH 4/6] Reduce connection timeouts --- ledgerwallet/transport/ble.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledgerwallet/transport/ble.py b/ledgerwallet/transport/ble.py index 00de0eb..d2d32fc 100644 --- a/ledgerwallet/transport/ble.py +++ b/ledgerwallet/transport/ble.py @@ -12,7 +12,7 @@ async def ble_discover(): - devices = await BleakScanner.discover(2) + devices = await BleakScanner.discover(timeout=1.) return devices @@ -22,7 +22,7 @@ def callback(sender, data): async def _get_client(ble_address: str) -> BleakClient: - device = await BleakScanner.find_device_by_address(ble_address, timeout=2.0) + device = await BleakScanner.find_device_by_address(ble_address, timeout=1.0) if not device: raise BleakError(f"Device with address {ble_address} could not be found.") From 48e6e7b01586c324b8f8f9e47c3b25fa38e649bd Mon Sep 17 00:00:00 2001 From: yhql Date: Fri, 1 Jul 2022 10:34:02 +0200 Subject: [PATCH 5/6] Guard BLE usage with an env variable --- README.md | 4 ++++ ledgerwallet/transport/ble.py | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index af7ba6a..1329849 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,9 @@ $ ledgerctl -v run Bitcoin => e0d8000007426974636f696e <= 9000 ``` +### Using BLE + +BLE scanning is disabled by default. It can be activated by setting an environment variable named `LEDGER_USE_BLE`. ## Contributing @@ -156,3 +159,4 @@ And executed with: ```console pre-commit run --all-files ``` + diff --git a/ledgerwallet/transport/ble.py b/ledgerwallet/transport/ble.py index d2d32fc..df27aa2 100644 --- a/ledgerwallet/transport/ble.py +++ b/ledgerwallet/transport/ble.py @@ -1,4 +1,5 @@ import asyncio +import os from bleak import BleakClient, BleakScanner from bleak.exc import BleakError from typing import List @@ -98,14 +99,17 @@ def __init__(self, device): @classmethod def enumerate_devices(cls): - loop = asyncio.get_event_loop() - discovered_devices = loop.run_until_complete(ble_discover()) - devices = [] - for device in discovered_devices: - if device.name is not None: - if device.name.startswith("Nano X"): - devices.append(BleDevice(device)) - return devices + if "LEDGER_USE_BLE" in os.environ: + loop = asyncio.get_event_loop() + discovered_devices = loop.run_until_complete(ble_discover()) + devices = [] + for device in discovered_devices: + if device.name is not None: + if device.name.startswith("Nano X"): + devices.append(BleDevice(device)) + return devices + else: + return [] def __str__(self): return "[BLE Device] {} ({})".format(self.device.name, self.device.address) From 3ba98a8f82a31fb804fee641da118879af556e50 Mon Sep 17 00:00:00 2001 From: yhql Date: Fri, 3 Mar 2023 10:34:32 +0100 Subject: [PATCH 6/6] Add BLE support with 'bleak' --- README.md | 2 +- ledgerwallet/client.py | 4 +++- ledgerwallet/transport/__init__.py | 2 +- ledgerwallet/transport/ble.py | 5 +++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1329849..6bd309f 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ $ ledgerctl -v run Bitcoin => e0d8000007426974636f696e <= 9000 ``` + ### Using BLE BLE scanning is disabled by default. It can be activated by setting an environment variable named `LEDGER_USE_BLE`. @@ -159,4 +160,3 @@ And executed with: ```console pre-commit run --all-files ``` - diff --git a/ledgerwallet/client.py b/ledgerwallet/client.py index c8b6317..09fd810 100644 --- a/ledgerwallet/client.py +++ b/ledgerwallet/client.py @@ -164,6 +164,7 @@ class NoLedgerDeviceException(Exception): class LedgerClient(object): def __init__(self, device=None, cla=0xE0, private_key=None): + self.device = None if device is None: devices = enumerate_devices() if len(devices) == 0: @@ -184,7 +185,8 @@ def __del__(self): self.close() def close(self): - self.device.close() + if self.device is not None: + self.device.close() def raw_exchange(self, data: bytes) -> bytes: LOG.debug("=> " + data.hex()) diff --git a/ledgerwallet/transport/__init__.py b/ledgerwallet/transport/__init__.py index 4ea567b..e339bb8 100644 --- a/ledgerwallet/transport/__init__.py +++ b/ledgerwallet/transport/__init__.py @@ -1,7 +1,7 @@ from contextlib import contextmanager -from .device import Device from .ble import BleDevice +from .device import Device from .hid import HidDevice from .tcp import TcpDevice diff --git a/ledgerwallet/transport/ble.py b/ledgerwallet/transport/ble.py index df27aa2..1ce661e 100644 --- a/ledgerwallet/transport/ble.py +++ b/ledgerwallet/transport/ble.py @@ -1,8 +1,9 @@ import asyncio import os +from typing import List + from bleak import BleakClient, BleakScanner from bleak.exc import BleakError -from typing import List HANDLE_CHAR_ENABLE_NOTIF = 13 HANDLE_CHAR_WRITE = 16 @@ -13,7 +14,7 @@ async def ble_discover(): - devices = await BleakScanner.discover(timeout=1.) + devices = await BleakScanner.discover(timeout=1.0) return devices