Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BLE support #25

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ $ ledgerctl -v run Bitcoin
<= 9000
```

### Using BLE

BLE scanning is disabled by default. It can be activated by setting an environment variable named `LEDGER_USE_BLE`.

## Contributing

### Pre-commit checks
Expand Down
8 changes: 7 additions & 1 deletion ledgerwallet/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,14 @@ 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:
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
Expand All @@ -179,8 +181,12 @@ 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()
if self.device is not None:
self.device.close()

def raw_exchange(self, data: bytes) -> bytes:
LOG.debug("=> " + data.hex())
Expand Down
3 changes: 2 additions & 1 deletion ledgerwallet/transport/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from contextlib import contextmanager

from .ble import BleDevice
from .device import Device
from .hid import HidDevice
from .tcp import TcpDevice

DEVICE_CLASSES = [TcpDevice, HidDevice]
DEVICE_CLASSES = [TcpDevice, HidDevice, BleDevice]


def enumerate_devices():
Expand Down
137 changes: 137 additions & 0 deletions ledgerwallet/transport/ble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import asyncio
import os
from typing import List

from bleak import BleakClient, BleakScanner
from bleak.exc import BleakError

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(timeout=1.0)
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=1.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):
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)

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()
3 changes: 3 additions & 0 deletions ledgerwallet/transport/hid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
3 changes: 3 additions & 0 deletions ledgerwallet/transport/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ classifiers = [
dynamic = ["version", "description"]
requires-python = ">=3.7"
dependencies = [
"bleak",
"click >=8.0",
"construct >=2.10",
"cryptography >=2.5",
Expand Down