Skip to content

Commit

Permalink
Replace libusbsio with hid
Browse files Browse the repository at this point in the history
This commit is extracted from: Nitrokey/pynitrokey#523

This introduces a dependency on the hidapi library.  If the library is
not present when trying to list or open a LPC55 bootloader device, an
exception will be raised.

Co-authored-by: Sosthène Guédon <[email protected]>
  • Loading branch information
robin-nitrokey and sosthene-nitrokey committed Aug 6, 2024
1 parent da1137e commit 2197fad
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 291 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Other Changes

- Vendor `spsdk` dependency to reduce the total number of dependencies
- Replace `libusbsio` dependency with `hid` (requires `hidapi` library)

## [v0.1.0](https://github.com/Nitrokey/nitrokey-sdk-py/releases/tag/v0.1.0) (2024-07-29)

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ for device in NKPK.list():
The Nitrokey Python SDK currently requires Python 3.9 or later.
Support for old Python versions may be dropped in minor releases.

## Dependencies

The Nitrokey Python SDK requires the following libraries for Nitrokey 3 bootloader communication:
- [hidapi][]

[hidapi]: https://github.com/libusb/hidapi

## Related Projects

- [pynitrokey](https://github.com/Nitrokey/pynitrokey):
Expand Down
24 changes: 12 additions & 12 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ tlv8 = "^0.10"
# lpc55
crcmod = "^1.7"
cryptography = ">=42"
libusbsio = "^2.1"
hid = "^1.0.3"

# nrf52
ecdsa = "^0.19"
Expand Down Expand Up @@ -68,8 +68,3 @@ ignore_errors = true
[[tool.mypy.overrides]]
module = "nitrokey.trussed._bootloader.nrf52"
disallow_untyped_calls = false

# libusbsio is used by lpc55_upload, will be replaced eventually
[[tool.mypy.overrides]]
module = ["libusbsio.*"]
ignore_missing_imports = true
7 changes: 2 additions & 5 deletions src/nitrokey/trussed/_bootloader/lpc55.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .lpc55_upload.mboot.properties import PropertyTag
from .lpc55_upload.sbfile.sb2.images import BootImageV21
from .lpc55_upload.utils.interfaces.device.usb_device import UsbDevice
from .lpc55_upload.utils.usbfilter import USBDeviceFilter

RKTH = bytes.fromhex("050aad3e77791a81e59c5b2ba5a158937e9460ee325d8ccba09734b8fdebb171")
KEK = bytes([0xAA] * 32)
Expand Down Expand Up @@ -101,9 +100,8 @@ def update(

@classmethod
def _list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]:
device_filter = USBDeviceFilter(f"0x{vid:x}:0x{pid:x}")
devices = []
for device in UsbDevice.enumerate(device_filter):
for device in UsbDevice.enumerate(vid=vid, pid=pid):
try:
devices.append(cls(device))
except ValueError:
Expand All @@ -114,8 +112,7 @@ def _list_vid_pid(cls: type[T], vid: int, pid: int) -> list[T]:

@classmethod
def _open(cls: type[T], path: str) -> Optional[T]:
device_filter = USBDeviceFilter(path)
devices = UsbDevice.enumerate(device_filter)
devices = UsbDevice.enumerate(path=path)
if len(devices) == 0:
logger.warn(f"No HID device at {path}")
return None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

"""Low level Hid device."""
import logging
from typing import List, Optional

import libusbsio
from typing import TYPE_CHECKING, List, Optional

from ....exceptions import SPSDKConnectionError, SPSDKError
from ....utils.exceptions import SPSDKTimeoutError
from ....utils.interfaces.device.base import DeviceBase
from ....utils.usbfilter import USBDeviceFilter

if TYPE_CHECKING:
import hid

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -43,10 +43,7 @@ def __init__(
self.product_name = product_name or ""
self.interface_number = interface_number or 0
self._timeout = timeout or 2000
libusbsio_logger = logging.getLogger("libusbsio")
self._device: libusbsio.LIBUSBSIO.HID_DEVICE = libusbsio.usbsio(
loglevel=libusbsio_logger.getEffectiveLevel()
).HIDAPI_DeviceCreate()
self._device: Optional["hid.Device"] = None

@property
def timeout(self) -> int:
Expand All @@ -64,21 +61,22 @@ def is_opened(self) -> bool:
:return: True if device is open, False othervise.
"""
return self._opened
return self._device is not None

def open(self) -> None:
"""Open the interface.
:raises SPSDKError: if device is already opened
:raises SPSDKConnectionError: if the device can not be opened
"""
import hid

logger.debug(f"Opening the Interface: {str(self)}")
if self.is_opened:
# This would get HID_DEVICE into broken state
raise SPSDKError("Can't open already opened device")
try:
self._device.Open(self.path)
self._opened = True
self._device = hid.Device(path=self.path)
except Exception as error:
raise SPSDKConnectionError(
f"Unable to open device '{str(self)}'"
Expand All @@ -91,10 +89,10 @@ def close(self) -> None:
:raises SPSDKConnectionError: if the device can not be opened
"""
logger.debug(f"Closing the Interface: {str(self)}")
if self.is_opened:
if self._device is not None:
try:
self._device.Close()
self._opened = False
self._device.close()
self._device = None
except Exception as error:
raise SPSDKConnectionError(
f"Unable to close device '{str(self)}'"
Expand All @@ -110,14 +108,14 @@ def read(self, length: int, timeout: Optional[int] = None) -> bytes:
:raises SPSDKTimeoutError: Time-out
"""
timeout = timeout or self.timeout
if not self.is_opened:
if self._device is None:
raise SPSDKConnectionError("Device is not opened for reading")
try:
(data, result) = self._device.Read(length, timeout_ms=timeout)
data = self._device.read(length, timeout=timeout)
except Exception as e:
raise SPSDKConnectionError(str(e)) from e
if not data:
logger.error(f"Cannot read from HID device, error={result}")
logger.error("Cannot read from HID device")
raise SPSDKTimeoutError()
assert isinstance(data, bytes)
return data
Expand All @@ -130,10 +128,10 @@ def write(self, data: bytes, timeout: Optional[int] = None) -> None:
:raises SPSDKConnectionError: Sending data to device failure
"""
timeout = timeout or self.timeout
if not self.is_opened:
if self._device is None:
raise SPSDKConnectionError("Device is not opened for writing")
try:
bytes_written = self._device.Write(data, timeout_ms=timeout)
bytes_written = self._device.write(data)
except Exception as e:
raise SPSDKConnectionError(str(e)) from e
if bytes_written < 0 or bytes_written < len(data):
Expand All @@ -153,30 +151,26 @@ def __hash__(self) -> int:

@classmethod
def enumerate(
cls, usb_device_filter: USBDeviceFilter, timeout: Optional[int] = None
cls,
vid: Optional[int] = None,
pid: Optional[int] = None,
path: Optional[str] = None,
) -> List["UsbDevice"]:
"""Get list of all connected devices which matches device_id.
"""Get list of all connected devices which matches device_id."""
import hid

:param usb_device_filter: USBDeviceFilter object
:param timeout: Default timeout to be set
:return: List of interfaces found
"""
devices = []
libusbsio_logger = logging.getLogger("libusbsio")
sio = libusbsio.usbsio(loglevel=libusbsio_logger.getEffectiveLevel())
all_hid_devices = sio.HIDAPI_Enumerate()

# iterate on all devices found
for dev in all_hid_devices:
if usb_device_filter.compare(vars(dev)) is True:
for dev in hid.enumerate(vid=vid or 0, pid=pid or 0):
if path is None or dev["path"] == path.encode():
new_device = cls(
vid=dev["vendor_id"],
pid=dev["product_id"],
path=dev["path"],
vendor_name=dev["manufacturer_string"],
product_name=dev["product_string"],
interface_number=dev["interface_number"],
timeout=timeout,
)
devices.append(new_device)
return devices
11 changes: 0 additions & 11 deletions src/nitrokey/trussed/_bootloader/lpc55_upload/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
# SPDX-License-Identifier: BSD-3-Clause

"""Miscellaneous functions used throughout the SPSDK."""
import hashlib
import logging
import re
from enum import Enum
from math import ceil
Expand All @@ -15,8 +13,6 @@

from ..exceptions import SPSDKError, SPSDKValueError

logger = logging.getLogger(__name__)


class Endianness(str, Enum):
"""Endianness enum."""
Expand Down Expand Up @@ -262,10 +258,3 @@ def swap16(x: int) -> int:
if x < 0 or x > 0xFFFF:
raise SPSDKError("Incorrect number to be swapped")
return ((x << 8) & 0xFF00) | ((x >> 8) & 0x00FF)


def get_hash(text: Union[str, bytes]) -> str:
"""Returns hash of given text."""
if isinstance(text, str):
text = text.encode("utf-8")
return hashlib.sha1(text).digest().hex()[:8]
Loading

0 comments on commit 2197fad

Please sign in to comment.