ledger_lib/transport/
usb.rs

1//! USB HID transport implementation
2
3use std::{ffi::CString, fmt::Display, io::ErrorKind, marker::PhantomData, time::Duration};
4
5use hidapi::{HidApi, HidDevice, HidError};
6use tracing::{debug, error, trace, warn};
7
8use crate::{
9    info::{LedgerInfo, Model},
10    transport::PhantomNonSend,
11    Error, NonSendExchange, Transport,
12};
13
14/// Basic USB device information
15#[derive(Clone, PartialEq, Debug)]
16#[cfg_attr(feature = "clap", derive(clap::Parser))]
17pub struct UsbInfo {
18    #[cfg_attr(feature = "clap", clap(long, value_parser=u16_parse_hex))]
19    /// USB Device Vendor ID (VID) in hex
20    pub vid: u16,
21
22    #[cfg_attr(feature = "clap", clap(long, value_parser=u16_parse_hex))]
23    /// USB Device Product ID (PID) in hex
24    pub pid: u16,
25
26    #[cfg_attr(feature = "clap", clap(long))]
27    /// Device path
28    pub path: Option<String>,
29}
30
31impl Display for UsbInfo {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        write!(f, "{:04x}:{:04x}", self.vid, self.pid)
34    }
35}
36
37/// Helper to pass VID/PID pairs from hex values
38#[cfg(feature = "clap")]
39fn u16_parse_hex(s: &str) -> Result<u16, std::num::ParseIntError> {
40    u16::from_str_radix(s, 16)
41}
42
43/// USB HID based transport
44///
45/// This type is deliberately non-`Send` to avoid potential quirks that might happen when
46/// the underlying `hidapi` type changes threads.
47/// If you don't need low-level control, see [LedgerProvider](crate::LedgerProvider) for a tokio-based wrapper.
48pub struct UsbTransport {
49    hid_api: HidApi,
50    _phantom: PhantomNonSend,
51}
52
53/// USB HID based device
54///
55/// This type is deliberately non-`Send` to avoid potential quirks that might happen when
56/// the underlying `hidapi` type changes threads.
57pub struct UsbDevice {
58    pub info: UsbInfo,
59    device: HidDevice,
60    _phantom: PhantomNonSend,
61}
62
63/// Ledger USB VID
64pub const LEDGER_VID: u16 = 0x2c97;
65
66/// HID usage page used by Ledger for its APDU interface.
67#[allow(unused)]
68const LEDGER_APDU_USAGE_PAGE: u16 = 0xffa0;
69/// The value of "interface_number" that Ledger's APDU interface will have.
70#[allow(unused)]
71const LEDGER_APDU_INTREFACE_NUMBER: i32 = 0;
72
73fn is_apdu_interface(device_info: &hidapi::DeviceInfo) -> bool {
74    // A Ledger device has two USB HID interfaces, one of which is the "APDU" interface
75    // and the other one FIDO/U2F; we need to select the former and ignore the latter.
76
77    // The more reliable way of selecting the APDU interface is to look for the corresponding
78    // "usage_page"; however, it's not available on Linux libusb backends, in which case
79    // we select the interface based on its interface number. This is similar to how it's done in Ledger Live -
80    // https://github.com/LedgerHQ/ledger-live/blob/4b73b3b61f07bc44fe4a04606e3cf1e610e7eb51/libs/ledgerjs/packages/hw-transport-node-hid-noevents/src/TransportNodeHid.ts#L19-L22
81    // except that in Ledger Live they always resort to using the interface number on Linux, but
82    // here it's done only for Linux/libusb.
83    // Note: on Windows, the FIDO interface is never returned by `hidapi::HidApi::devices_list`
84    // for some reason; presumably it's because the interface is held by the OS. But it's still
85    // better to do the filtering "just in case" and also for consistency with Ledger Live.
86
87    #[cfg(all(feature = "transport_usb_libusb", target_os = "linux"))]
88    {
89        let is_apdu = device_info.interface_number() == LEDGER_APDU_INTREFACE_NUMBER;
90        debug!(
91            "(PID={pid:#x}) USB interface #{inum} is APDU: {is_apdu}",
92            pid = device_info.product_id(),
93            inum = device_info.interface_number()
94        );
95        is_apdu
96    }
97
98    #[cfg(not(all(feature = "transport_usb_libusb", target_os = "linux")))]
99    {
100        let is_apdu = device_info.usage_page() == LEDGER_APDU_USAGE_PAGE;
101        debug!(
102            "(PID={pid:#x}) USB interface #{inum} (usage page = {uspg:#x}) is APDU: {is_apdu}",
103            pid = device_info.product_id(),
104            inum = device_info.interface_number(),
105            uspg = device_info.usage_page(),
106        );
107        is_apdu
108    }
109}
110
111impl UsbTransport {
112    /// Create a new [UsbTransport]
113    pub fn new() -> Result<Self, Error> {
114        #[cfg(feature = "transport_usb_libusb")]
115        debug!("Feature transport_usb_libusb is enabled");
116
117        #[cfg(feature = "transport_usb_hidraw")]
118        debug!("Feature transport_usb_hidraw is enabled");
119
120        Ok(Self {
121            hid_api: HidApi::new()?,
122            _phantom: PhantomData,
123        })
124    }
125}
126
127impl Transport for UsbTransport {
128    type Filters = ();
129    type Info = UsbInfo;
130    type Device = UsbDevice;
131
132    /// List available devices using the [UsbTransport]
133    async fn list(&mut self, _filters: Self::Filters) -> Result<Vec<LedgerInfo>, Error> {
134        debug!("Listing USB devices");
135
136        // Refresh available devices
137        // TODO: determine whether the refresh call is critical (or, useful?)
138        if let Err(e) = self.hid_api.refresh_devices() {
139            warn!("Failed to refresh devices: {e:?}");
140        }
141
142        tokio::time::sleep(Duration::from_millis(200)).await;
143
144        // Fetch list of devices, filtering for ledgers
145        let devices: Vec<_> = self
146            .hid_api
147            .device_list()
148            .filter(|d| d.vendor_id() == LEDGER_VID && is_apdu_interface(d))
149            .map(|d| LedgerInfo {
150                model: Model::from_usb_pid(d.product_id()),
151                conn: UsbInfo {
152                    vid: d.vendor_id(),
153                    pid: d.product_id(),
154                    path: Some(d.path().to_string_lossy().to_string()),
155                }
156                .into(),
157            })
158            .collect();
159
160        debug!("devices: {:?}", devices);
161
162        Ok(devices)
163    }
164
165    /// Connect to a device using the usb transport
166    async fn connect(&mut self, info: UsbInfo) -> Result<UsbDevice, Error> {
167        debug!("Connecting to USB device: {:?}", info);
168
169        // If we have a path, use this to connect
170        let d = if let Some(p) = &info.path {
171            let p = CString::new(p.clone()).unwrap();
172            self.hid_api.open_path(&p)
173
174        // Otherwise, fallback to (non unique!) vid:pid
175        } else {
176            self.hid_api.open(info.vid, info.pid)
177        };
178
179        match d {
180            Ok(d) => {
181                debug!("Connected to USB device: {:?}", info);
182                Ok(UsbDevice {
183                    device: d,
184                    info,
185                    _phantom: PhantomData,
186                })
187            }
188            Err(e) => {
189                debug!("Failed to connect to USB device: {:?}", e);
190                Err(e.into())
191            }
192        }
193    }
194}
195
196// HID packet length (header + data)
197const HID_PACKET_LEN: usize = 64;
198
199// Five bytes: channel (0x101), tag (0x05), sequence index
200const HID_HEADER_LEN: usize = 5;
201
202impl UsbDevice {
203    /// Write an APDU to the device
204    pub fn write(&mut self, apdu: &[u8]) -> Result<(), Error> {
205        debug!("Write APDU");
206
207        // Setup outgoing data buffer with length prefix
208        let mut data = Vec::with_capacity(apdu.len() + 2);
209        data.extend_from_slice(&(apdu.len() as u16).to_be_bytes());
210        data.extend_from_slice(apdu);
211
212        debug!("TX: {:02x?}", data);
213
214        // Write data in 64 byte chunks
215        for (i, c) in data.chunks(HID_PACKET_LEN - HID_HEADER_LEN).enumerate() {
216            trace!("Writing chunk {} of {} bytes", i, c.len());
217
218            // Setup HID packet with header and data
219            let mut packet = Vec::with_capacity(HID_PACKET_LEN + 1);
220
221            // Zero prefix for unknown reasons
222            packet.push(0x00);
223
224            // Header channel (0x101), tag (0x05), sequence index
225            packet.extend_from_slice(&[0x01, 0x01, 0x05]);
226            packet.extend_from_slice(&(i as u16).to_be_bytes());
227            // Remaining data
228            packet.extend_from_slice(c);
229
230            trace!("Write: 0x{:02x?}", packet);
231
232            // Write HID packet
233            self.device.write(&packet)?;
234        }
235
236        Ok(())
237    }
238
239    /// Read an APDU from the device
240    pub fn read(&mut self, timeout: Duration) -> Result<Vec<u8>, Error> {
241        debug!("Read APDU");
242
243        let mut buff = [0u8; HID_PACKET_LEN + 1];
244
245        // Read first chunk of response
246        // Timeout argument applied here as once the reply has started timeout bounds should be more consistent
247        let n = match self
248            .device
249            .read_timeout(&mut buff, timeout.as_millis() as i32)
250        {
251            Ok(n) => n,
252            Err(HidError::IoError { error }) if error.kind() == ErrorKind::TimedOut => {
253                return Err(Error::Timeout)
254            }
255            Err(e) => return Err(e.into()),
256        };
257
258        // Check read length is valid for following operations
259        if n == 0 {
260            error!("Empty response");
261            return Err(Error::EmptyResponse);
262        } else if n < 7 {
263            error!("Unexpected read length {n}");
264            return Err(Error::UnexpectedResponse);
265        }
266
267        // Check header matches expectations
268        if buff[..5] != [0x01, 0x01, 0x05, 0x00, 0x00] {
269            error!("Unexpected response header: {:02x?}", &buff[..5]);
270            return Err(Error::UnexpectedResponse);
271        }
272
273        trace!("initial read: {buff:02x?}");
274
275        // Parse response length
276        let len = u16::from_be_bytes([buff[5], buff[6]]) as usize;
277
278        trace!("Read len: {len}");
279
280        // Setup response buffer and add any remaining data
281        let mut resp = Vec::with_capacity(len);
282
283        let data_len = len.min(n - 7);
284        resp.extend_from_slice(&buff[7..][..data_len]);
285
286        // Read following chunks if required
287        let mut seq_idx = 1;
288        while resp.len() < len {
289            let rem = len - resp.len();
290
291            trace!("Read chunk {seq_idx} ({rem} bytes remaining)");
292
293            // Read next chunk, constant timeout as chunks should be sent end-to-end
294            let n = self.device.read_timeout(&mut buff, 500)?;
295
296            if n < 5 {
297                error!("Invalid chunk length {n}");
298                return Err(Error::UnexpectedResponse);
299            }
300
301            // Check header and sequence index
302            if buff[..3] != [0x01, 0x01, 0x05] {
303                error!("Unexpected response header: {:02x?}", &buff[..5]);
304                return Err(Error::UnexpectedResponse);
305            }
306            if u16::from_be_bytes([buff[3], buff[4]]) != seq_idx {
307                error!("Unexpected sequence index: {:02x?}", &buff[5..7]);
308                return Err(Error::UnexpectedResponse);
309            }
310
311            // Add to response buffer
312            let data_len = rem.min(n - 5);
313            resp.extend_from_slice(&buff[5..][..data_len]);
314            seq_idx += 1;
315        }
316
317        debug!("RX: {:02x?}", resp);
318
319        Ok(resp)
320    }
321
322    pub(crate) async fn is_connected(&self) -> Result<bool, Error> {
323        Ok(self.device.get_device_info().is_ok())
324    }
325}
326
327/// [NonSendExchange] impl for sending APDUs to a [UsbDevice]
328impl NonSendExchange for UsbDevice {
329    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
330        // Write APDU command, chunked for HID transport
331        self.write(command)?;
332        // Read APDU response, chunked for HID transport
333        self.read(timeout)
334    }
335}