ledger_lib/transport/
usb.rs

1//! USB HID transport implementation
2//!
3//! # SAFETY
4//!
5//! This is _not_ `Send` or thread safe, see [transport][crate::transport] docs for
6//! more details.
7//!
8
9use std::{ffi::CString, fmt::Display, io::ErrorKind, time::Duration};
10
11use hidapi::{HidApi, HidDevice, HidError};
12use tracing::{debug, error, trace, warn};
13
14use crate::{
15    info::{LedgerInfo, Model},
16    Error,
17};
18
19use super::{Exchange, Transport};
20
21/// Basic USB device information
22#[derive(Clone, PartialEq, Debug)]
23#[cfg_attr(feature = "clap", derive(clap::Parser))]
24pub struct UsbInfo {
25    #[cfg_attr(feature = "clap", clap(long, value_parser=u16_parse_hex))]
26    /// USB Device Vendor ID (VID) in hex
27    pub vid: u16,
28
29    #[cfg_attr(feature = "clap", clap(long, value_parser=u16_parse_hex))]
30    /// USB Device Product ID (PID) in hex
31    pub pid: u16,
32
33    #[cfg_attr(feature = "clap", clap(long))]
34    /// Device path
35    pub path: Option<String>,
36}
37
38impl Display for UsbInfo {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "{:04x}:{:04x}", self.vid, self.pid)
41    }
42}
43
44/// Helper to pass VID/PID pairs from hex values
45#[cfg(feature = "clap")]
46fn u16_parse_hex(s: &str) -> Result<u16, std::num::ParseIntError> {
47    u16::from_str_radix(s, 16)
48}
49
50/// USB HID based transport
51///
52/// # Safety
53/// Due to `hidapi` this is not thread safe an only one instance must exist in an application.
54/// If you don't need low-level control see [crate::LedgerProvider] for a tokio based wrapper.
55pub struct UsbTransport {
56    hid_api: HidApi,
57}
58
59/// USB HID based device
60pub struct UsbDevice {
61    pub info: UsbInfo,
62    device: HidDevice,
63}
64
65/// Ledger USB VID
66pub const LEDGER_VID: u16 = 0x2c97;
67
68impl UsbTransport {
69    /// Create a new [UsbTransport]
70    pub fn new() -> Result<Self, Error> {
71        Ok(Self {
72            hid_api: HidApi::new()?,
73        })
74    }
75}
76
77// With the unstable_async_trait feature we can (correctly) mark this as non-send
78// however [async_trait] can't easily differentiate between send and non-send so we're
79// exposing this as Send for the moment
80
81#[cfg(feature = "unstable_async_trait")]
82impl !Send for UsbDevice {}
83#[cfg(feature = "unstable_async_trait")]
84impl !Sync for UsbDevice {}
85
86#[cfg(feature = "unstable_async_trait")]
87impl !Send for UsbTransport {}
88#[cfg(feature = "unstable_async_trait")]
89impl !Sync for UsbTransport {}
90
91/// WARNING: THIS IS A LIE TO APPEASE `async_trait`
92#[cfg(not(feature = "unstable_async_trait"))]
93unsafe impl Send for UsbTransport {}
94
95#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
96impl Transport for UsbTransport {
97    type Filters = ();
98    type Info = UsbInfo;
99    type Device = UsbDevice;
100
101    /// List available devices using the [UsbTransport]
102    async fn list(&mut self, _filters: Self::Filters) -> Result<Vec<LedgerInfo>, Error> {
103        debug!("Listing USB devices");
104
105        // Refresh available devices
106        // TODO: determine whether the refresh call is critical (or, useful?)
107        if let Err(e) = self.hid_api.refresh_devices() {
108            warn!("Failed to refresh devices: {e:?}");
109        }
110
111        tokio::time::sleep(Duration::from_millis(200)).await;
112
113        // Fetch list of devices, filtering for ledgers
114        let devices: Vec<_> = self
115            .hid_api
116            .device_list()
117            .filter(|d| d.vendor_id() == LEDGER_VID)
118            .map(|d| LedgerInfo {
119                model: Model::from_pid(d.product_id()),
120                conn: UsbInfo {
121                    vid: d.vendor_id(),
122                    pid: d.product_id(),
123                    path: Some(d.path().to_string_lossy().to_string()),
124                }
125                .into(),
126            })
127            .collect();
128
129        debug!("devices: {:?}", devices);
130
131        Ok(devices)
132    }
133
134    /// Connect to a device using the usb transport
135    async fn connect(&mut self, info: UsbInfo) -> Result<UsbDevice, Error> {
136        debug!("Connecting to USB device: {:?}", info);
137
138        // If we have a path, use this to connect
139        let d = if let Some(p) = &info.path {
140            let p = CString::new(p.clone()).unwrap();
141            self.hid_api.open_path(&p)
142
143        // Otherwise, fallback to (non unique!) vid:pid
144        } else {
145            self.hid_api.open(info.vid, info.pid)
146        };
147
148        match d {
149            Ok(d) => {
150                debug!("Connected to USB device: {:?}", info);
151                Ok(UsbDevice { device: d, info })
152            }
153            Err(e) => {
154                debug!("Failed to connect to USB device: {:?}", e);
155                Err(e.into())
156            }
157        }
158    }
159}
160
161// HID packet length (header + data)
162const HID_PACKET_LEN: usize = 64;
163
164// Five bytes: channnel (0x101), tag (0x05), sequence index
165const HID_HEADER_LEN: usize = 5;
166
167impl UsbDevice {
168    /// Write an APDU to the device
169    pub fn write(&mut self, apdu: &[u8]) -> Result<(), Error> {
170        debug!("Write APDU");
171
172        // Setup outgoing data buffer with length prefix
173        let mut data = Vec::with_capacity(apdu.len() + 2);
174        data.extend_from_slice(&(apdu.len() as u16).to_be_bytes());
175        data.extend_from_slice(apdu);
176
177        debug!("TX: {:02x?}", data);
178
179        // Write data in 64 byte chunks
180        for (i, c) in data.chunks(HID_PACKET_LEN - HID_HEADER_LEN).enumerate() {
181            trace!("Writing chunk {} of {} bytes", i, c.len());
182
183            // Setup HID packet with header and data
184            let mut packet = Vec::with_capacity(HID_PACKET_LEN + 1);
185
186            // Zero prefix for unknown reasons
187            packet.push(0x00);
188
189            // Header channnel (0x101), tag (0x05), sequence index
190            packet.extend_from_slice(&[0x01, 0x01, 0x05]);
191            packet.extend_from_slice(&(i as u16).to_be_bytes());
192            // Remaining data
193            packet.extend_from_slice(c);
194
195            trace!("Write: 0x{:02x?}", packet);
196
197            // Write HID packet
198            self.device.write(&packet)?;
199        }
200
201        Ok(())
202    }
203
204    /// Read an APDU from the device
205    pub fn read(&mut self, timeout: Duration) -> Result<Vec<u8>, Error> {
206        debug!("Read APDU");
207
208        let mut buff = [0u8; HID_PACKET_LEN + 1];
209
210        // Read first chunk of response
211        // Timeout argument applied here as once the reply has started timeout bounds should be more consistent
212        let n = match self
213            .device
214            .read_timeout(&mut buff, timeout.as_millis() as i32)
215        {
216            Ok(n) => n,
217            Err(HidError::IoError { error }) if error.kind() == ErrorKind::TimedOut => {
218                return Err(Error::Timeout)
219            }
220            Err(e) => return Err(e.into()),
221        };
222
223        // Check read length is valid for following operations
224        if n == 0 {
225            error!("Empty response");
226            return Err(Error::EmptyResponse);
227        } else if n < 7 {
228            error!("Unexpected read length {n}");
229            return Err(Error::UnexpectedResponse);
230        }
231
232        // Check header matches expectations
233        if buff[..5] != [0x01, 0x01, 0x05, 0x00, 0x00] {
234            error!("Unexpected response header: {:02x?}", &buff[..5]);
235            return Err(Error::UnexpectedResponse);
236        }
237
238        trace!("initial read: {buff:02x?}");
239
240        // Parse response length
241        let len = u16::from_be_bytes([buff[5], buff[6]]) as usize;
242
243        trace!("Read len: {len}");
244
245        // Setup response buffer and add any remaining data
246        let mut resp = Vec::with_capacity(len);
247
248        let data_len = len.min(n - 7);
249        resp.extend_from_slice(&buff[7..][..data_len]);
250
251        // Read following chunks if required
252        let mut seq_idx = 1;
253        while resp.len() < len {
254            let rem = len - resp.len();
255
256            trace!("Read chunk {seq_idx} ({rem} bytes remaining)");
257
258            // Read next chunk, constant timeout as chunks should be sent end-to-end
259            let n = self.device.read_timeout(&mut buff, 500)?;
260
261            if n < 5 {
262                error!("Invalid chunk length {n}");
263                return Err(Error::UnexpectedResponse);
264            }
265
266            // Check header and sequence index
267            if buff[..3] != [0x01, 0x01, 0x05] {
268                error!("Unexpected response header: {:02x?}", &buff[..5]);
269                return Err(Error::UnexpectedResponse);
270            }
271            if u16::from_be_bytes([buff[3], buff[4]]) != seq_idx {
272                error!("Unexpected sequence index: {:02x?}", &buff[5..7]);
273                return Err(Error::UnexpectedResponse);
274            }
275
276            // Add to response buffer
277            let data_len = rem.min(n - 5);
278            resp.extend_from_slice(&buff[5..][..data_len]);
279            seq_idx += 1;
280        }
281
282        debug!("RX: {:02x?}", resp);
283
284        Ok(resp)
285    }
286
287    pub(crate) async fn is_connected(&self) -> Result<bool, Error> {
288        Ok(self.device.get_device_info().is_ok())
289    }
290}
291
292/// [Exchange] impl for sending APDUs to a [UsbDevice]
293#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
294impl Exchange for UsbDevice {
295    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
296        // Write APDU command, chunked for HID transport
297        self.write(command)?;
298        // Read APDU response, chunked for HID transport
299        self.read(timeout)
300    }
301}