From d36829babb2591b1e804d8451e3ca87074a4ed2e Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 11 Nov 2025 16:46:43 +0200 Subject: [PATCH 1/6] Filter out non-APDU interfaces in UsbTransport --- cli/Cargo.toml | 9 ++++++- lib/Cargo.toml | 2 +- lib/src/provider/mod.rs | 4 +-- lib/src/transport/usb.rs | 57 +++++++++++++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 276e678..3e4b100 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -7,6 +7,13 @@ version = "0.1.0" edition = "2021" license = "Apache-2.0" +[features] +transport_usb_libusb = [ "ledger-lib/transport_usb_libusb" ] +transport_usb_hidraw = [ "ledger-lib/transport_usb_hidraw" ] + +# Same default as in lib/Cargo.toml +default = [ "transport_usb_libusb" ] + [dependencies] clap = { version = "4.2", features = [ "derive" ] } anyhow = "1.0" @@ -18,5 +25,5 @@ humantime = "2.1" hex = "0.4" serde_json = "1.0" -ledger-lib = { version = "0.1", features = [ "clap" ] } +ledger-lib = { version = "0.1", default-features = false, features = [ "clap", "transport_usb", "transport_tcp", "transport_ble" ] } ledger-proto = { version = "0.1" } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 6ea7e4f..c9a03b9 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -13,7 +13,7 @@ transport_usb = [ "hidapi" ] transport_tcp = [] transport_ble = [ "btleplug" ] -# Switch libusb backends, `libusb` works better with WSL so we're using that by default +# Switch libusb backends, `libusb` works better with WSL and plain Linux so we're using that by default transport_usb_libusb = [ "hidapi/linux-static-libusb" ] transport_usb_hidraw = [ "hidapi/linux-static-hidraw" ] diff --git a/lib/src/provider/mod.rs b/lib/src/provider/mod.rs index 5763c9b..dee04b9 100644 --- a/lib/src/provider/mod.rs +++ b/lib/src/provider/mod.rs @@ -99,7 +99,7 @@ impl Transport for LedgerProvider { .send((LedgerReq::List(filters), tx)) .map_err(|_| Error::Unknown)?; - // Await resposne + // Await response match rx.recv().await { Some(LedgerResp::Devices(i)) => Ok(i), Some(LedgerResp::Error(e)) => Err(e), @@ -116,7 +116,7 @@ impl Transport for LedgerProvider { .send((LedgerReq::Connect(info.clone()), tx)) .map_err(|_| Error::Unknown)?; - // Await resposne + // Await response match rx.recv().await { Some(LedgerResp::Handle(index)) => Ok(LedgerHandle { info, diff --git a/lib/src/transport/usb.rs b/lib/src/transport/usb.rs index ca36c15..20349f0 100644 --- a/lib/src/transport/usb.rs +++ b/lib/src/transport/usb.rs @@ -65,9 +65,60 @@ pub struct UsbDevice { /// Ledger USB VID pub const LEDGER_VID: u16 = 0x2c97; +/// HID usage page used by Ledger for its APDU interface. +#[allow(unused)] +const LEDGER_APDU_USAGE_PAGE: u16 = 0xffa0; +/// The value of "interface_number" that Ledger's APDU interface will have. +#[allow(unused)] +const LEDGER_APDU_INTREFACE_NUMBER: i32 = 0; + +fn is_apdu_interface(device_info: &hidapi::DeviceInfo) -> bool { + // A Ledger device has two USB HID interfaces, one of which is the "APDU" interface + // and the other one FIDO/U2F; we need to select the former and ignore the latter. + + // The more reliable way of selecting the APDU interface is to look for the corresponding + // "usage_page"; however, it's not available on Linux libusb backends, in which case + // we select the interface based on its interface number. This is similar to how it's done in Ledger Live - + // https://github.com/LedgerHQ/ledger-live/blob/4b73b3b61f07bc44fe4a04606e3cf1e610e7eb51/libs/ledgerjs/packages/hw-transport-node-hid-noevents/src/TransportNodeHid.ts#L19-L22 + // except that in Ledger Live they always resort to using the interface number on Linux, but + // here it's done only for Linux/libusb. + // Note: on Windows, the FIDO interface is never returned by `hidapi::HidApi::devices_list` + // for some reason; presumably it's because the interface is held by the OS. But it's still + // better to do the filtering "just in case" and also for consistency with Ledger Live. + + #[cfg(all(feature = "transport_usb_libusb", target_os = "linux"))] + { + let is_apdu = device_info.interface_number() == LEDGER_APDU_INTREFACE_NUMBER; + debug!( + "(PID={pid:#x}) USB interface #{inum} is APDU: {is_apdu}", + pid = device_info.product_id(), + inum = device_info.interface_number() + ); + is_apdu + } + + #[cfg(not(all(feature = "transport_usb_libusb", target_os = "linux")))] + { + let is_apdu = device_info.usage_page() == LEDGER_APDU_USAGE_PAGE; + debug!( + "(PID={pid:#x}) USB interface #{inum} (usage page = {uspg:#x}) is APDU: {is_apdu}", + pid = device_info.product_id(), + inum = device_info.interface_number(), + uspg = device_info.usage_page(), + ); + is_apdu + } +} + impl UsbTransport { /// Create a new [UsbTransport] pub fn new() -> Result { + #[cfg(feature = "transport_usb_libusb")] + debug!("Feature transport_usb_libusb is enabled"); + + #[cfg(feature = "transport_usb_hidraw")] + debug!("Feature transport_usb_hidraw is enabled"); + Ok(Self { hid_api: HidApi::new()?, }) @@ -114,7 +165,7 @@ impl Transport for UsbTransport { let devices: Vec<_> = self .hid_api .device_list() - .filter(|d| d.vendor_id() == LEDGER_VID) + .filter(|d| d.vendor_id() == LEDGER_VID && is_apdu_interface(d)) .map(|d| LedgerInfo { model: Model::from_pid(d.product_id()), conn: UsbInfo { @@ -161,7 +212,7 @@ impl Transport for UsbTransport { // HID packet length (header + data) const HID_PACKET_LEN: usize = 64; -// Five bytes: channnel (0x101), tag (0x05), sequence index +// Five bytes: channel (0x101), tag (0x05), sequence index const HID_HEADER_LEN: usize = 5; impl UsbDevice { @@ -186,7 +237,7 @@ impl UsbDevice { // Zero prefix for unknown reasons packet.push(0x00); - // Header channnel (0x101), tag (0x05), sequence index + // Header channel (0x101), tag (0x05), sequence index packet.extend_from_slice(&[0x01, 0x01, 0x05]); packet.extend_from_slice(&(i as u16).to_be_bytes()); // Remaining data From 4e871efa75d41dc97b07d506359a7b0dda5a9411 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 24 Nov 2025 12:11:30 +0200 Subject: [PATCH 2/6] Improve BLE device discovery, support new models --- lib/Cargo.toml | 24 +++--- lib/src/error.rs | 6 ++ lib/src/info.rs | 160 +++++++++++++++++++++++++++++++++++---- lib/src/transport/ble.rs | 90 +++++++--------------- lib/src/transport/tcp.rs | 2 +- lib/src/transport/usb.rs | 2 +- 6 files changed, 192 insertions(+), 92 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c9a03b9..b9a20b5 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -26,24 +26,22 @@ unstable_async_trait = [] default = [ "transport_usb", "transport_tcp", "transport_ble", "transport_usb_libusb" ] [dependencies] - -thiserror = "2.0" +async-trait = "0.1" +btleplug = { version = "0.11", optional = true } +clap = { version = "4.2", optional = true } +displaydoc = "0.2" encdec = "0.10" +futures = "0.3" +hidapi = { version = "2.1", optional = true, default-features = false } +lazy_static = "1.5" ledger-proto = { version = "0.1", default-features = false, features = [ "std" ] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +once_cell = "1.17" strum = { version = "0.26", features = ["derive"] } +thiserror = "2.0" tokio = { version = "1.27", features = ["full"] } -once_cell = "1.17" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = "1.3" -futures = "0.3" -async-trait = "0.1" -displaydoc = "0.2" - -clap = { version = "4.2", optional = true } -hidapi = { version = "2.1", optional = true, default-features = false } -btleplug = { version = "0.11", optional = true } - [dev-dependencies] anyhow = "1.0" diff --git a/lib/src/error.rs b/lib/src/error.rs index 956d270..fd52055 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -57,6 +57,12 @@ pub enum Error { #[error("Already running application ({0})")] ApplicationLoaded(String), + + #[error("Cannot read BLE device properties")] + CannotReadBleDeviceProperties, + + #[error("Cannot find BLE device specs")] + CannotFindBleDeviceSpecs, } impl From for Error { diff --git a/lib/src/info.rs b/lib/src/info.rs index deaad3e..139fa96 100644 --- a/lib/src/info.rs +++ b/lib/src/info.rs @@ -1,6 +1,9 @@ //! Device information types and connection filters +use std::collections::BTreeMap; + use strum::{Display, EnumString}; +use uuid::{uuid, Uuid}; use crate::Filters; @@ -37,7 +40,7 @@ impl LedgerInfo { } /// Ledger device models -#[derive(Clone, PartialEq, Debug, Display, EnumString)] +#[derive(Copy, Clone, PartialEq, Debug, Display, EnumString)] pub enum Model { /// Nano S NanoS, @@ -47,27 +50,156 @@ pub enum Model { NanoX, /// Stax Stax, + /// Flex + Flex, + /// Nano Gen5 + NanoGen5, /// Unknown model - Unknown(u16), + Unknown { usb_pid: Option }, } impl Model { /// Convert a USB PID to a [Model] kind - /// - /// Note that ledger PIDs vary depending on the device state so only the top byte is used - /// for matching. - pub fn from_pid(pid: u16) -> Model { - match pid & 0xFF00 { - // TODO: support all the models - //0x0001 => Ok(Model::NanoS), - 0x4000 => Model::NanoX, - 0x5000 => Model::NanoSPlus, - //0x0006 => Ok(Model::Stax), - _ => Model::Unknown(pid), - } + pub fn from_usb_pid(usb_pid: u16) -> Model { + INTERNAL_DEVICE_INFOS + .iter() + .find_map(|device_info| { + device_info + .matches_usb_pid(usb_pid) + .then_some(device_info.model) + }) + .unwrap_or_else(|| Model::Unknown { + usb_pid: Some(usb_pid), + }) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct BleSpec { + pub service_uuid: Uuid, + pub notify_uuid: Uuid, + pub write_uuid: Uuid, + pub write_cmd_uuid: Uuid, +} + +struct InternalDeviceInfo { + model: Model, + legacy_usb_product_id: u16, + product_id_mm: u16, + ble_specs: Vec, +} + +impl InternalDeviceInfo { + fn matches_usb_pid(&self, usb_pid: u16) -> bool { + // First compare the passed pid with the legacy product id, if that doesn't match, use product_id_mm. + // The logic was taken from here: + // https://github.com/LedgerHQ/ledger-live/blob/b870b8018319b7489c39c92e743adcfd4e33e948/libs/ledgerjs/packages/devices/src/index.ts#L190 + // (the legacy product id match will probably only work for some early variants of NanoS, but it's + // still better to be consistent with Ledger Live, just in case). + usb_pid == self.legacy_usb_product_id || usb_pid >> 8 == self.product_id_mm } } +// The table was taken from here: +// https://github.com/LedgerHQ/ledger-live/blob/b870b8018319b7489c39c92e743adcfd4e33e948/libs/ledgerjs/packages/devices/src/index.ts#L41 +lazy_static::lazy_static! { + static ref INTERNAL_DEVICE_INFOS: Vec = { + vec![ + InternalDeviceInfo { + model: Model::NanoS, + legacy_usb_product_id: 0x0001, + product_id_mm: 0x10, + ble_specs: vec![], + }, + InternalDeviceInfo { + model: Model::NanoX, + legacy_usb_product_id: 0x0004, + product_id_mm: 0x40, + ble_specs: vec![ + BleSpec { + service_uuid: uuid!("13d63400-2c97-0004-0000-4c6564676572"), + notify_uuid: uuid!("13d63400-2c97-0004-0001-4c6564676572"), + write_uuid: uuid!("13d63400-2c97-0004-0002-4c6564676572"), + write_cmd_uuid: uuid!("13d63400-2c97-0004-0003-4c6564676572"), + }, + ], + }, + InternalDeviceInfo { + model: Model::NanoSPlus, + legacy_usb_product_id: 0x0005, + product_id_mm: 0x50, + ble_specs: vec![], + }, + InternalDeviceInfo { + model: Model::Stax, + legacy_usb_product_id: 0x0006, + product_id_mm: 0x60, + ble_specs: vec![ + BleSpec { + service_uuid: uuid!("13d63400-2c97-6004-0000-4c6564676572"), + notify_uuid: uuid!("13d63400-2c97-6004-0001-4c6564676572"), + write_uuid: uuid!("13d63400-2c97-6004-0002-4c6564676572"), + write_cmd_uuid: uuid!("13d63400-2c97-6004-0003-4c6564676572"), + }, + ], + }, + InternalDeviceInfo { + model: Model::Flex, + legacy_usb_product_id: 0x0007, + product_id_mm: 0x70, + ble_specs: vec![ + BleSpec { + service_uuid: uuid!("13d63400-2c97-3004-0000-4c6564676572"), + notify_uuid: uuid!("13d63400-2c97-3004-0001-4c6564676572"), + write_uuid: uuid!("13d63400-2c97-3004-0002-4c6564676572"), + write_cmd_uuid: uuid!("13d63400-2c97-3004-0003-4c6564676572"), + }, + ], + }, + InternalDeviceInfo { + model: Model::NanoGen5, + legacy_usb_product_id: 0x0008, + product_id_mm: 0x80, + ble_specs: vec![ + BleSpec { + service_uuid: uuid!("13d63400-2c97-8004-0000-4c6564676572"), + notify_uuid: uuid!("13d63400-2c97-8004-0001-4c6564676572"), + write_uuid: uuid!("13d63400-2c97-8004-0002-4c6564676572"), + write_cmd_uuid: uuid!("13d63400-2c97-8004-0003-4c6564676572"), + }, + ], + }, + ] + }; + + static ref BLE_SPECS_BY_SERVICE_UUID: BTreeMap = { + INTERNAL_DEVICE_INFOS + .iter() + .flat_map(|dev_info| dev_info.ble_specs.iter()) + .map(|ble_spec| (ble_spec.service_uuid, ble_spec)) + .collect() + }; + + static ref INTERNAL_DEVICE_INFOS_BY_BLE_SERVICE_UUID: BTreeMap = { + INTERNAL_DEVICE_INFOS + .iter() + .flat_map(|dev_info| { + dev_info.ble_specs.iter().map(move |ble_spec| (ble_spec.service_uuid, dev_info)) + }) + .collect() + }; +} + +pub fn model_by_ble_service_uuid(service_uuid: &Uuid) -> Option { + INTERNAL_DEVICE_INFOS_BY_BLE_SERVICE_UUID + .get(service_uuid) + .map(|dev_info| dev_info.model) +} + +pub fn ble_spec_by_service_uuid(service_uuid: &Uuid) -> Option<&'static BleSpec> { + BLE_SPECS_BY_SERVICE_UUID.get(service_uuid).copied() +} + /// Ledger connection information #[derive(Clone, PartialEq, Debug)] pub enum ConnInfo { diff --git a/lib/src/transport/ble.rs b/lib/src/transport/ble.rs index 7969562..c79be6e 100644 --- a/lib/src/transport/ble.rs +++ b/lib/src/transport/ble.rs @@ -4,18 +4,17 @@ use std::{fmt::Display, pin::Pin, time::Duration}; use btleplug::{ api::{ - BDAddr, Central as _, Characteristic, Manager as _, Peripheral, ScanFilter, - ValueNotification, WriteType, + BDAddr, Central as _, Characteristic, Manager as _, Peripheral, ValueNotification, + WriteType, }, platform::Manager, }; use futures::{stream::StreamExt, Stream}; use tracing::{debug, error, trace, warn}; -use uuid::{uuid, Uuid}; use super::{Exchange, Transport}; use crate::{ - info::{ConnInfo, LedgerInfo, Model}, + info::{ble_spec_by_service_uuid, model_by_ble_service_uuid, ConnInfo, LedgerInfo}, Error, }; @@ -47,35 +46,6 @@ pub struct BleDevice { c_read: Characteristic, } -/// Bluetooth spec for ledger devices -/// see: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts#L32 -#[derive(Clone, PartialEq, Debug)] -struct BleSpec { - pub model: Model, - pub service_uuid: Uuid, - pub notify_uuid: Uuid, - pub write_uuid: Uuid, - pub write_cmd_uuid: Uuid, -} - -/// Spec for types of bluetooth device -const BLE_SPECS: &[BleSpec] = &[ - BleSpec { - model: Model::NanoX, - service_uuid: uuid!("13d63400-2c97-0004-0000-4c6564676572"), - notify_uuid: uuid!("13d63400-2c97-0004-0001-4c6564676572"), - write_uuid: uuid!("13d63400-2c97-0004-0002-4c6564676572"), - write_cmd_uuid: uuid!("13d63400-2c97-0004-0003-4c6564676572"), - }, - BleSpec { - model: Model::Stax, - service_uuid: uuid!("13d63400-2c97-6004-0000-4c6564676572"), - notify_uuid: uuid!("13d63400-2c97-6004-0001-4c6564676572"), - write_uuid: uuid!("13d63400-2c97-6004-0002-4c6564676572"), - write_cmd_uuid: uuid!("13d63400-2c97-6004-0003-4c6564676572"), - }, -]; - impl BleTransport { pub async fn new() -> Result { // Setup connection manager @@ -97,16 +67,16 @@ impl BleTransport { // Grab adapter list let adapters = self.manager.adapters().await?; - // TODO: load filters? - let f = ScanFilter { services: vec![] }; - // Search using adapters for adapter in adapters.iter() { let info = adapter.adapter_info().await?; debug!("Scan with adapter {info}"); - // Start scan with adaptor - adapter.start_scan(f.clone()).await?; + // Start scan with adaptor. + // Note: filtering by service uuids at this level works fine on Linux, but doesn't work + // on Windows for some reason (an empty peripherals list is returned). + // So we pass an empty filter and do the actual filtering manually later. + adapter.start_scan(Default::default()).await?; tokio::time::sleep(duration).await; @@ -131,29 +101,22 @@ impl BleTransport { } }; - // Skip peripherals without a local name (NanoX should report this) - let name = match &properties.local_name { - Some(v) => v, - None => continue, - }; - debug!("Peripheral: {p:?} props: {properties:?}"); - // Match on peripheral names - let model = if name.contains("Nano X") { - Model::NanoX - } else if name.contains("Stax") { - Model::Stax - } else { + let Some(model) = properties + .services + .iter() + .find_map(model_by_ble_service_uuid) + else { continue; }; // Add to device list matched.push(( LedgerInfo { - model: model.clone(), + model, conn: BleInfo { - name: name.clone(), + name: properties.local_name.unwrap_or(String::new()), addr: properties.address, } .into(), @@ -212,17 +175,20 @@ impl Transport for BleTransport { let name = &i.name; // Fetch properties - let properties = p.properties().await?; + let properties = p + .properties() + .await? + .ok_or(Error::CannotReadBleDeviceProperties)?; + + debug!("peripheral {name}: {p:?} properties: {properties:?}"); // Connect to device and subscribe to characteristics - // Fetch specs for matched model (contains characteristic identifiers) - let specs = match BLE_SPECS.iter().find(|s| s.model == d.model) { - Some(v) => v, - None => { - warn!("No specs for model: {:?}", d.model); - return Err(Error::Unknown); - } - }; + // Fetch specs for matched uuid (contains characteristic identifiers) + let specs = properties + .services + .iter() + .find_map(ble_spec_by_service_uuid) + .ok_or(Error::CannotFindBleDeviceSpecs)?; // If we're not connected, attempt to connect if !p.is_connected().await? { @@ -237,8 +203,6 @@ impl Transport for BleTransport { } } - debug!("peripheral {name}: {p:?} properties: {properties:?}"); - // Then, grab available services and locate characteristics p.discover_services().await?; diff --git a/lib/src/transport/tcp.rs b/lib/src/transport/tcp.rs index 7a3799b..4d4ee4d 100644 --- a/lib/src/transport/tcp.rs +++ b/lib/src/transport/tcp.rs @@ -78,7 +78,7 @@ impl Transport for TcpTransport { Err(_) => { devices.push(LedgerInfo { conn: TcpInfo { addr }.into(), - model: Model::Unknown(0), + model: Model::Unknown { usb_pid: None }, }); } } diff --git a/lib/src/transport/usb.rs b/lib/src/transport/usb.rs index 20349f0..ff67f87 100644 --- a/lib/src/transport/usb.rs +++ b/lib/src/transport/usb.rs @@ -167,7 +167,7 @@ impl Transport for UsbTransport { .device_list() .filter(|d| d.vendor_id() == LEDGER_VID && is_apdu_interface(d)) .map(|d| LedgerInfo { - model: Model::from_pid(d.product_id()), + model: Model::from_usb_pid(d.product_id()), conn: UsbInfo { vid: d.vendor_id(), pid: d.product_id(), From 8f037861310f2410446af51defd7af971aa83f05 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 24 Nov 2025 20:24:33 +0200 Subject: [PATCH 3/6] Replace Error::Unknown with more specific errors; remove unused errors UnknownModel, ApplicationLoaded; add .vscode to .gitignore --- .gitignore | 1 + lib/src/error.rs | 32 +++++++++++++++++++++++++------- lib/src/provider/context.rs | 4 ++-- lib/src/provider/mod.rs | 21 +++++++++++++++------ lib/src/transport/ble.rs | 10 +++++----- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 2c96eb1..bd8f42d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ Cargo.lock +.vscode diff --git a/lib/src/error.rs b/lib/src/error.rs index fd52055..7242346 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -17,11 +17,23 @@ pub enum Error { #[error(transparent)] Ble(#[from] btleplug::Error), - #[error("Unknown ledger model: {0}")] - UnknownModel(u16), + #[error("Attempted to send APDU to unknown device handle")] + ApduSentToUnknownDeviceHandle, - #[error("Unknown error")] - Unknown, + #[error("Request channel closed")] + RequestChannelClosed, + + #[error("Request response channel closed")] + RequestResponseChannelClosed, + + #[error("Unexpected response while listing devices")] + UnexpectedResponseWhileListingDevices, + + #[error("Unexpected response while connecting")] + UnexpectedResponseWhileConnecting, + + #[error("Unexpected response while exchanging data")] + UnexpectedResponseWhileExchangingData, #[error("No devices found")] NoDevices, @@ -55,14 +67,20 @@ pub enum Error { #[error("Device in use")] DeviceInUse, - #[error("Already running application ({0})")] - ApplicationLoaded(String), - #[error("Cannot read BLE device properties")] CannotReadBleDeviceProperties, #[error("Cannot find BLE device specs")] CannotFindBleDeviceSpecs, + + #[error("The BLE device is still not connected after a successful connect")] + NotConnectedAfterSuccessfulBleConnect, + + #[error("Missing read or write BLE characteristics")] + MissingReadOrWriteBleCharacteristics, + + #[error("Unexpected MTU response")] + UnexpectedMtuResponse, } impl From for Error { diff --git a/lib/src/provider/context.rs b/lib/src/provider/context.rs index 85d739a..da3a0d8 100644 --- a/lib/src/provider/context.rs +++ b/lib/src/provider/context.rs @@ -89,7 +89,7 @@ impl ProviderImpl { Ok(v) => v, Err(e) => { error!("Failed to create transport: {}", e); - return Err(Error::Unknown); + return Err(e); } }; @@ -183,7 +183,7 @@ impl ProviderImpl { Some(d) => d, None => { error!("Attempted to send APDU to unknown device handle: {}", index); - return Some(LedgerResp::Error(Error::Unknown)); + return Some(LedgerResp::Error(Error::ApduSentToUnknownDeviceHandle)); } }; diff --git a/lib/src/provider/mod.rs b/lib/src/provider/mod.rs index dee04b9..ab88280 100644 --- a/lib/src/provider/mod.rs +++ b/lib/src/provider/mod.rs @@ -97,13 +97,16 @@ impl Transport for LedgerProvider { // Send control request self.req_tx .send((LedgerReq::List(filters), tx)) - .map_err(|_| Error::Unknown)?; + .map_err(|_| Error::RequestChannelClosed)?; // Await response match rx.recv().await { Some(LedgerResp::Devices(i)) => Ok(i), Some(LedgerResp::Error(e)) => Err(e), - _ => Err(Error::Unknown), + Some(LedgerResp::Resp(_) | LedgerResp::Handle(_)) => { + Err(Error::UnexpectedResponseWhileListingDevices) + } + None => Err(Error::RequestResponseChannelClosed), } } @@ -114,7 +117,7 @@ impl Transport for LedgerProvider { // Send control request self.req_tx .send((LedgerReq::Connect(info.clone()), tx)) - .map_err(|_| Error::Unknown)?; + .map_err(|_| Error::RequestChannelClosed)?; // Await response match rx.recv().await { @@ -124,7 +127,10 @@ impl Transport for LedgerProvider { req_tx: self.req_tx.clone(), }), Some(LedgerResp::Error(e)) => Err(e), - _ => Err(Error::Unknown), + Some(LedgerResp::Devices(_) | LedgerResp::Resp(_)) => { + Err(Error::UnexpectedResponseWhileConnecting) + } + None => Err(Error::RequestResponseChannelClosed), } } } @@ -138,13 +144,16 @@ impl Exchange for LedgerHandle { // Send APDU request self.req_tx .send((LedgerReq::Req(self.index, command.to_vec(), timeout), tx)) - .map_err(|_| Error::Unknown)?; + .map_err(|_| Error::RequestChannelClosed)?; // Await APDU response match rx.recv().await { Some(LedgerResp::Resp(data)) => Ok(data), Some(LedgerResp::Error(e)) => Err(e), - _ => Err(Error::Unknown), + Some(LedgerResp::Devices(_) | LedgerResp::Handle(_)) => { + Err(Error::UnexpectedResponseWhileExchangingData) + } + None => Err(Error::RequestResponseChannelClosed), } } } diff --git a/lib/src/transport/ble.rs b/lib/src/transport/ble.rs index c79be6e..ac5389f 100644 --- a/lib/src/transport/ble.rs +++ b/lib/src/transport/ble.rs @@ -194,12 +194,12 @@ impl Transport for BleTransport { if !p.is_connected().await? { if let Err(e) = p.connect().await { warn!("Failed to connect to {name}: {e:?}"); - return Err(Error::Unknown); + return Err(Error::Ble(e)); } if !p.is_connected().await? { warn!("Not connected to {name}"); - return Err(Error::Unknown); + return Err(Error::NotConnectedAfterSuccessfulBleConnect); } } @@ -217,7 +217,7 @@ impl Transport for BleTransport { (Some(w), Some(r)) => (w, r), _ => { error!("Failed to match read and write characteristics for {name}"); - return Err(Error::Unknown); + return Err(Error::MissingReadOrWriteBleCharacteristics); } }; @@ -356,11 +356,11 @@ impl BleDevice { } Some(r) => { warn!("Unexpected MTU response: {r:02x?}"); - return Err(Error::Unknown); + return Err(Error::UnexpectedMtuResponse); } None => { warn!("Failed to request MTU"); - return Err(Error::Unknown); + return Err(Error::Closed); } }; From 36673bb806085ced1a86fa14a49c1c804bc97fbc Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Tue, 25 Nov 2025 00:11:33 +0200 Subject: [PATCH 4/6] UsbTransport and UsbDevice are no longer Send. Remove explicit "impl !Send" and "impl Send" for UsbTransport/UsbDevice. --- lib/Cargo.toml | 7 ++--- lib/src/device.rs | 17 ++++++------ lib/src/lib.rs | 47 +++++++++++++++++++------------ lib/src/provider/context.rs | 4 +-- lib/src/provider/mod.rs | 2 -- lib/src/transport/ble.rs | 5 +--- lib/src/transport/mod.rs | 29 +++++++------------ lib/src/transport/tcp.rs | 6 +--- lib/src/transport/usb.rs | 55 +++++++++++++------------------------ proto/src/status.rs | 2 +- 10 files changed, 73 insertions(+), 101 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b9a20b5..726ef7c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -6,6 +6,7 @@ keywords = [ "ledger", "wallet", "usb", "hid", "bluetooth" ] version = "0.1.0" edition = "2021" license = "Apache-2.0" +rust-version = "1.75" # Needed for "async fn" and "impl Trait" in traits. [features] # Select enabled transports @@ -20,19 +21,15 @@ transport_usb_hidraw = [ "hidapi/linux-static-hidraw" ] # Enable `clap` attributes on exported objects clap = [ "dep:clap" ] -# enable `async_fn_in_trait` nightly feature, removes need for `async_trait` macros -unstable_async_trait = [] - default = [ "transport_usb", "transport_tcp", "transport_ble", "transport_usb_libusb" ] [dependencies] -async-trait = "0.1" btleplug = { version = "0.11", optional = true } clap = { version = "4.2", optional = true } displaydoc = "0.2" encdec = "0.10" futures = "0.3" -hidapi = { version = "2.1", optional = true, default-features = false } +hidapi = { version = "2.6", optional = true, default-features = false } lazy_static = "1.5" ledger-proto = { version = "0.1", default-features = false, features = [ "std" ] } once_cell = "1.17" diff --git a/lib/src/device.rs b/lib/src/device.rs index 8315e27..60ca37d 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -15,18 +15,18 @@ use ledger_proto::{ use crate::{ info::{AppInfo, DeviceInfo}, - Error, Exchange, + Error, NonSendExchange, }; const APDU_BUFF_LEN: usize = 256; -/// [Device] provides a high-level interface exchanging APDU objects with implementers of [Exchange] -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] +/// [Device] provides a high-level interface exchanging APDU objects with implementers of [NonSendExchange]. +#[allow(async_fn_in_trait)] pub trait Device { - /// Issue a request APDU, returning a reponse APDU + /// Issue a request APDU, returning a response APDU async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>( &mut self, - request: impl ApduReq<'a> + Send, + request: impl ApduReq<'a>, buff: &'b mut [u8], timeout: Duration, ) -> Result; @@ -106,13 +106,12 @@ pub trait Device { } } -/// Generic [Device] implementation for types supporting [Exchange] -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] -impl Device for T { +/// Generic [Device] implementation for types supporting [NonSendExchange] +impl Device for T { /// Issue a request APDU to a device, encoding and decoding internally then returning a response APDU async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>( &mut self, - req: impl ApduReq<'a> + Send, + req: impl ApduReq<'a>, buff: &'b mut [u8], timeout: Duration, ) -> Result { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b9048c0..f422cce 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,23 +2,20 @@ //! //! [Device] provides a high-level API for exchanging APDUs with Ledger devices using the [ledger_proto] traits. //! This is suitable for extension with application-specific interface traits, and automatically -//! implemented over [Exchange] for low-level byte exchange with devices. +//! implemented over [NonSendExchange] for low-level byte exchange with devices. //! //! [LedgerProvider] and [LedgerHandle] provide a high-level tokio-compatible [Transport] //! for application integration, supporting connecting to and interacting with ledger devices. -//! This uses a pinned thread to avoid thread safety issues with `hidapi` and async executors. //! //! Low-level [Transport] implementations are provided for [USB/HID](transport::UsbTransport), //! [BLE](transport::BleTransport) and [TCP](transport::TcpTransport), with a [Generic](transport::GenericTransport) //! implementation providing a common interface over all enabled transports. //! -//! ## Safety -//! -//! Transports are currently marked as `Send` due to limitations of [async_trait] and are NOT all -//! thread safe. If you're calling this from an async context, please use [LedgerProvider]. -//! -//! This will be corrected when the unstable async trait feature is stabilised, -//! which until then can be opted-into using the `unstable_async_trait` feature +//! Note that futures produced by async methods of [Transport] and [Device] are not `Send`, i.e. they +//! can't be used with multi-threaded async executors. The reason is that [UsbTransport](transport::UsbTransport) +//! and [UsbDevice](transport::UsbDevice) are not `Send`. This is a (probably redundant) precaution +//! against potential quirks that might occur when the underlying `hidapi` objects change threads.\ +//! In a multi-threaded async environment use [LedgerProvider] and [LedgerHandle] instead. //! //! ## Examples //! @@ -49,10 +46,7 @@ //! } //! ``` -#![cfg_attr(feature = "unstable_async_trait", feature(async_fn_in_trait))] -#![cfg_attr(feature = "unstable_async_trait", feature(negative_impls))] - -use std::time::Duration; +use std::{future::Future, time::Duration}; use tracing::debug; @@ -76,7 +70,7 @@ pub use provider::{LedgerHandle, LedgerProvider}; mod device; pub use device::Device; -/// Default timeout helper for use with [Device] and [Exchange] +/// Default timeout helper for use with [Device] and [Exchange]/[NonSendExchange] pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); /// Device discovery filter @@ -95,20 +89,37 @@ pub enum Filters { Ble, } -/// [Exchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] +/// [Exchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices. pub trait Exchange { - async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error>; + fn exchange( + &mut self, + command: &[u8], + timeout: Duration, + ) -> impl Future, Error>> + Send; } /// Blanket [Exchange] impl for mutable references -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Exchange for &mut T { async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error> { ::exchange(self, command, timeout).await } } +/// [NonSendExchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices. +/// +/// It is the same as [Exchange], but it doesn't enforce the `Send` bound on the returned future. +#[allow(async_fn_in_trait)] +pub trait NonSendExchange { + async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error>; +} + +/// Blanket [NonSendExchange] impl for types that implement [Exchange]. +impl NonSendExchange for T { + async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error> { + ::exchange(self, command, timeout).await + } +} + /// Launch an application by name and return a device handle. /// /// This checks whether an application is running, exits this if it diff --git a/lib/src/provider/context.rs b/lib/src/provider/context.rs index da3a0d8..927eb2a 100644 --- a/lib/src/provider/context.rs +++ b/lib/src/provider/context.rs @@ -11,7 +11,7 @@ use crate::{ error::Error, provider::{LedgerReq, LedgerResp, ReqChannel}, transport::{GenericDevice, GenericTransport, Transport}, - Exchange, + NonSendExchange, }; /// Context for provider task @@ -188,7 +188,7 @@ impl ProviderImpl { }; // Issue APDU request to device and return response - match Exchange::exchange(d, apdu, *timeout).await { + match NonSendExchange::exchange(d, apdu, *timeout).await { Ok(r) => LedgerResp::Resp(r), Err(e) => LedgerResp::Error(e), } diff --git a/lib/src/provider/mod.rs b/lib/src/provider/mod.rs index ab88280..4b2074e 100644 --- a/lib/src/provider/mod.rs +++ b/lib/src/provider/mod.rs @@ -84,7 +84,6 @@ impl LedgerProvider { } /// [Transport] implementation for high-level [LedgerProvider] -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Transport for LedgerProvider { type Device = LedgerHandle; type Info = LedgerInfo; @@ -136,7 +135,6 @@ impl Transport for LedgerProvider { } /// [Exchange] implementation for [LedgerProvider] backed [LedgerHandle] -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Exchange for LedgerHandle { async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error> { let (tx, mut rx) = unbounded_channel::(); diff --git a/lib/src/transport/ble.rs b/lib/src/transport/ble.rs index ac5389f..378f0f9 100644 --- a/lib/src/transport/ble.rs +++ b/lib/src/transport/ble.rs @@ -12,10 +12,9 @@ use btleplug::{ use futures::{stream::StreamExt, Stream}; use tracing::{debug, error, trace, warn}; -use super::{Exchange, Transport}; use crate::{ info::{ble_spec_by_service_uuid, model_by_ble_service_uuid, ConnInfo, LedgerInfo}, - Error, + Error, Exchange, Transport, }; /// Transport for listing and connecting to BLE connected Ledger devices @@ -131,7 +130,6 @@ impl BleTransport { } /// [Transport] implementation for [BleTransport] -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Transport for BleTransport { type Filters = (); type Info = BleInfo; @@ -377,7 +375,6 @@ impl BleDevice { } /// [Exchange] impl for BLE backed devices -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Exchange for BleDevice { async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error> { // Fetch notification channel for responses diff --git a/lib/src/transport/mod.rs b/lib/src/transport/mod.rs index 295aa30..8696b5c 100644 --- a/lib/src/transport/mod.rs +++ b/lib/src/transport/mod.rs @@ -2,17 +2,8 @@ //! //! Transports are gated by `transport_X` features, while [GenericTransport] and //! [GenericDevice] provide an abstraction over enabled transports. -//! -//! # Safety -//! [UsbTransport] (and thus [GenericTransport] when `transport_usb` feature is enabled) -//! is _not_ `Send` or `Sync`, however this is marked as such to appease `async_trait`... -//! -//! Once `async_trait` has stabilised transports can be marked correctly. -//! (This is also implemented under the `unstable_async_trait` feature) -//! Until then, use [LedgerProvider](crate::LedgerProvider) for a `Sync + Send` interface or -//! be _super sure_ you're not going to call transports from a multi-threaded context. -use std::{fmt::Debug, time::Duration}; +use std::{fmt::Debug, marker::PhantomData, sync::MutexGuard, time::Duration}; #[cfg(feature = "transport_ble")] use tracing::warn; @@ -36,18 +27,21 @@ pub use tcp::{TcpDevice, TcpInfo, TcpTransport}; use crate::{ info::{ConnInfo, LedgerInfo}, - Error, Exchange, Filters, + Error, Filters, NonSendExchange, }; -/// [Transport] trait provides an abstract interface for transport implementations -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] +/// A PhantomData to force a type to be !Send +pub type PhantomNonSend = PhantomData>; + +/// [Transport] trait provides an abstract interface for transport implementations. +#[allow(async_fn_in_trait)] pub trait Transport { /// Connection filters type Filters: Default + Debug; /// Device information, used for listing and connecting type Info: Debug; /// Device handle for interacting with the device - type Device: Exchange; + type Device: NonSendExchange; /// List available devices async fn list(&mut self, filters: Self::Filters) -> Result, Error>; @@ -57,7 +51,6 @@ pub trait Transport { } /// Blanket [Transport] implementation for references types -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Transport for &mut T where ::Device: Send, @@ -103,7 +96,7 @@ pub enum GenericDevice { } impl GenericTransport { - /// Create a new [GenericTransport] with all endabled transports + /// Create a new [GenericTransport] with all enabled transports pub async fn new() -> Result { debug!("Initialising GenericTransport"); @@ -120,7 +113,6 @@ impl GenericTransport { } } -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Transport for GenericTransport { type Filters = Filters; type Info = LedgerInfo; @@ -202,8 +194,7 @@ impl GenericDevice { } } -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] -impl Exchange for GenericDevice { +impl NonSendExchange for GenericDevice { /// Exchange an APDU with the [GenericDevice] async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error> { match self { diff --git a/lib/src/transport/tcp.rs b/lib/src/transport/tcp.rs index 4d4ee4d..b41fedd 100644 --- a/lib/src/transport/tcp.rs +++ b/lib/src/transport/tcp.rs @@ -12,11 +12,9 @@ use tracing::{debug, error}; use crate::{ info::{LedgerInfo, Model}, - Error, + Error, Exchange, Transport, }; -use super::{Exchange, Transport}; - /// TCP transport implementation for interacting with Speculos via the TCP APDU socket #[derive(Default)] pub struct TcpTransport {} @@ -54,7 +52,6 @@ impl TcpTransport { } } -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Transport for TcpTransport { type Filters = (); type Info = TcpInfo; @@ -160,7 +157,6 @@ impl TcpDevice { } /// [Exchange] implementation for the TCP transport -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Exchange for TcpDevice { async fn exchange(&mut self, req: &[u8], timeout: Duration) -> Result, Error> { // Write APDU request diff --git a/lib/src/transport/usb.rs b/lib/src/transport/usb.rs index ff67f87..812ecb1 100644 --- a/lib/src/transport/usb.rs +++ b/lib/src/transport/usb.rs @@ -1,23 +1,16 @@ //! USB HID transport implementation -//! -//! # SAFETY -//! -//! This is _not_ `Send` or thread safe, see [transport][crate::transport] docs for -//! more details. -//! -use std::{ffi::CString, fmt::Display, io::ErrorKind, time::Duration}; +use std::{ffi::CString, fmt::Display, io::ErrorKind, marker::PhantomData, time::Duration}; use hidapi::{HidApi, HidDevice, HidError}; use tracing::{debug, error, trace, warn}; use crate::{ info::{LedgerInfo, Model}, - Error, + transport::PhantomNonSend, + Error, NonSendExchange, Transport, }; -use super::{Exchange, Transport}; - /// Basic USB device information #[derive(Clone, PartialEq, Debug)] #[cfg_attr(feature = "clap", derive(clap::Parser))] @@ -49,17 +42,22 @@ fn u16_parse_hex(s: &str) -> Result { /// USB HID based transport /// -/// # Safety -/// Due to `hidapi` this is not thread safe an only one instance must exist in an application. -/// If you don't need low-level control see [crate::LedgerProvider] for a tokio based wrapper. +/// This type is deliberately non-`Send` to avoid potential quirks that might happen when +/// the underlying `hidapi` type changes threads. +/// If you don't need low-level control, see [LedgerProvider](crate::LedgerProvider) for a tokio-based wrapper. pub struct UsbTransport { hid_api: HidApi, + _phantom: PhantomNonSend, } /// USB HID based device +/// +/// This type is deliberately non-`Send` to avoid potential quirks that might happen when +/// the underlying `hidapi` type changes threads. pub struct UsbDevice { pub info: UsbInfo, device: HidDevice, + _phantom: PhantomNonSend, } /// Ledger USB VID @@ -121,29 +119,11 @@ impl UsbTransport { Ok(Self { hid_api: HidApi::new()?, + _phantom: PhantomData, }) } } -// With the unstable_async_trait feature we can (correctly) mark this as non-send -// however [async_trait] can't easily differentiate between send and non-send so we're -// exposing this as Send for the moment - -#[cfg(feature = "unstable_async_trait")] -impl !Send for UsbDevice {} -#[cfg(feature = "unstable_async_trait")] -impl !Sync for UsbDevice {} - -#[cfg(feature = "unstable_async_trait")] -impl !Send for UsbTransport {} -#[cfg(feature = "unstable_async_trait")] -impl !Sync for UsbTransport {} - -/// WARNING: THIS IS A LIE TO APPEASE `async_trait` -#[cfg(not(feature = "unstable_async_trait"))] -unsafe impl Send for UsbTransport {} - -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] impl Transport for UsbTransport { type Filters = (); type Info = UsbInfo; @@ -199,7 +179,11 @@ impl Transport for UsbTransport { match d { Ok(d) => { debug!("Connected to USB device: {:?}", info); - Ok(UsbDevice { device: d, info }) + Ok(UsbDevice { + device: d, + info, + _phantom: PhantomData, + }) } Err(e) => { debug!("Failed to connect to USB device: {:?}", e); @@ -340,9 +324,8 @@ impl UsbDevice { } } -/// [Exchange] impl for sending APDUs to a [UsbDevice] -#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] -impl Exchange for UsbDevice { +/// [NonSendExchange] impl for sending APDUs to a [UsbDevice] +impl NonSendExchange for UsbDevice { async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result, Error> { // Write APDU command, chunked for HID transport self.write(command)?; diff --git a/proto/src/status.rs b/proto/src/status.rs index e02421c..a0d234b 100644 --- a/proto/src/status.rs +++ b/proto/src/status.rs @@ -1,6 +1,6 @@ /// Device status codes (two bytes, trailing response data) /// -/// Replicated from: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/errors/src/index.ts#L212 +/// Replicated from: #[derive(Copy, Clone, Debug, displaydoc::Display, num_enum::TryFromPrimitive)] #[repr(u16)] pub enum StatusCode { From 2baef522693123a7701e5c7076e23c9d379c50a4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 26 Nov 2025 17:40:55 +0200 Subject: [PATCH 5/6] Appease clippy --- lib/src/info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/info.rs b/lib/src/info.rs index 139fa96..10cb830 100644 --- a/lib/src/info.rs +++ b/lib/src/info.rs @@ -68,7 +68,7 @@ impl Model { .matches_usb_pid(usb_pid) .then_some(device_info.model) }) - .unwrap_or_else(|| Model::Unknown { + .unwrap_or(Model::Unknown { usb_pid: Some(usb_pid), }) } From 6f2e02ef894236a628b01cfa9cde7b589f015db0 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 14 Jan 2026 13:59:30 +0200 Subject: [PATCH 6/6] Futures returned by methods of Device are now Send. Bump min Rust version to 1.85 --- Cargo.toml | 4 ++++ cli/Cargo.toml | 1 + lib/Cargo.toml | 3 ++- lib/src/device.rs | 25 ++++++++++++++++++------- lib/src/lib.rs | 7 +++++-- proto/Cargo.toml | 1 + sim/Cargo.toml | 1 + 7 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 064d956..e9be3b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ members = [ "cli", ] +[workspace.package] +# 1.85+ is needed for its support of the 2024 edition, which is in turn needed by bluez-async-0.8.2 +rust-version = "1.85" + [patch.crates-io] ledger-proto = { path = "proto" } ledger-lib = { path = "lib" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3e4b100..c83fc15 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -6,6 +6,7 @@ keywords = [ "ledger", "wallet", "cli" ] version = "0.1.0" edition = "2021" license = "Apache-2.0" +rust-version.workspace = true [features] transport_usb_libusb = [ "ledger-lib/transport_usb_libusb" ] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 726ef7c..adfbee1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -6,7 +6,7 @@ keywords = [ "ledger", "wallet", "usb", "hid", "bluetooth" ] version = "0.1.0" edition = "2021" license = "Apache-2.0" -rust-version = "1.75" # Needed for "async fn" and "impl Trait" in traits. +rust-version.workspace = true [features] # Select enabled transports @@ -24,6 +24,7 @@ clap = [ "dep:clap" ] default = [ "transport_usb", "transport_tcp", "transport_ble", "transport_usb_libusb" ] [dependencies] +async-trait = "0.1" btleplug = { version = "0.11", optional = true } clap = { version = "4.2", optional = true } displaydoc = "0.2" diff --git a/lib/src/device.rs b/lib/src/device.rs index 60ca37d..56e8a79 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -2,6 +2,7 @@ use std::time::Duration; +use async_trait::async_trait; use encdec::{EncDec, Encode}; use tracing::{debug, error}; @@ -15,18 +16,27 @@ use ledger_proto::{ use crate::{ info::{AppInfo, DeviceInfo}, - Error, NonSendExchange, + Error, Exchange, }; const APDU_BUFF_LEN: usize = 256; -/// [Device] provides a high-level interface exchanging APDU objects with implementers of [NonSendExchange]. -#[allow(async_fn_in_trait)] +// Note: replacing the `async_trait` macro below with the "modern" syntax, i.e. +// fn foo(...) -> impl Future + Send { +// async move { ... } +// } +// results in a bunch of errors "lifetime bound not satisfied ... note: this is a known limitation +// that will be removed in the future (see issue #100013 for more information)". +// This happens with Rust 1.91 and below, while 1.92 is able to compile the code. +// So, `async_trait` serves as a workaround for this issue. + +/// [Device] provides a high-level interface exchanging APDU objects with implementers of [Exchange]. +#[async_trait] pub trait Device { /// Issue a request APDU, returning a response APDU async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>( &mut self, - request: impl ApduReq<'a>, + request: impl ApduReq<'a> + Send, buff: &'b mut [u8], timeout: Duration, ) -> Result; @@ -106,12 +116,13 @@ pub trait Device { } } -/// Generic [Device] implementation for types supporting [NonSendExchange] -impl Device for T { +/// Generic [Device] implementation for types supporting [Exchange] +#[async_trait] +impl Device for T { /// Issue a request APDU to a device, encoding and decoding internally then returning a response APDU async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>( &mut self, - req: impl ApduReq<'a>, + req: impl ApduReq<'a> + Send, buff: &'b mut [u8], timeout: Duration, ) -> Result { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f422cce..c7a6e2c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,7 +2,7 @@ //! //! [Device] provides a high-level API for exchanging APDUs with Ledger devices using the [ledger_proto] traits. //! This is suitable for extension with application-specific interface traits, and automatically -//! implemented over [NonSendExchange] for low-level byte exchange with devices. +//! implemented over [Exchange] for low-level byte exchange with devices. //! //! [LedgerProvider] and [LedgerHandle] provide a high-level tokio-compatible [Transport] //! for application integration, supporting connecting to and interacting with ledger devices. @@ -126,6 +126,9 @@ impl NonSendExchange for T { /// is not the desired application, then launches the specified app /// by name. /// +/// Note that this function is only usable with a `Transport` whose associated `Device` type +/// implements `Exchange` (e.g. `LedgerProvider`). +/// /// # WARNING /// Due to the constant re-enumeration of devices when changing app /// contexts, and the lack of reported serial numbers by ledger devices, @@ -140,7 +143,7 @@ pub async fn launch_app( ) -> Result<::Device, Error> where T: Transport + Send, - ::Device: Send, + ::Device: Exchange + Send, { let mut buff = [0u8; 256]; diff --git a/proto/Cargo.toml b/proto/Cargo.toml index fa300f3..75075f7 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -6,6 +6,7 @@ keywords = [ "ledger", "protocol", "apdu" ] version = "0.1.0" edition = "2021" license = "Apache-2.0" +rust-version.workspace = true [features] # `std` feature implements `std::error::Error` for `ApduError` type diff --git a/sim/Cargo.toml b/sim/Cargo.toml index c9afbd3..8e905f9 100644 --- a/sim/Cargo.toml +++ b/sim/Cargo.toml @@ -6,6 +6,7 @@ keywords = [ "ledger", "hardware", "wallet", "speculos", "simulator" ] version = "0.1.0" edition = "2021" license = "Apache-2.0" +rust-version.workspace = true [dependencies] bytes = "1.2"