ledger_transport_hid/
lib.rs

1/*******************************************************************************
2*   (c) 2022 Zondax AG
3*
4*  Licensed under the Apache License, Version 2.0 (the "License");
5*  you may not use this file except in compliance with the License.
6*  You may obtain a copy of the License at
7*
8*      http://www.apache.org/licenses/LICENSE-2.0
9*
10*  Unless required by applicable law or agreed to in writing, software
11*  distributed under the License is distributed on an "AS IS" BASIS,
12*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*  See the License for the specific language governing permissions and
14*  limitations under the License.
15********************************************************************************/
16mod errors;
17use std::{io::Cursor, ops::Deref, sync::Mutex};
18
19use byteorder::{BigEndian, ReadBytesExt};
20pub use errors::LedgerHIDError;
21pub use hidapi;
22use hidapi::{DeviceInfo, HidApi, HidDevice};
23use ledger_transport::{async_trait, APDUAnswer, APDUCommand, Exchange};
24use log::info;
25
26const LEDGER_VID: u16 = 0x2c97;
27const LEDGER_USAGE_PAGE: u16 = 0xFFA0;
28const LEDGER_CHANNEL: u16 = 0x0101;
29// for Windows compatability, we prepend the buffer with a 0x00
30// so the actual buffer is 64 bytes
31const LEDGER_PACKET_WRITE_SIZE: u8 = 65;
32const LEDGER_PACKET_READ_SIZE: u8 = 64;
33const LEDGER_TIMEOUT: i32 = 10_000_000;
34
35pub struct TransportNativeHID {
36    device: Mutex<HidDevice>,
37}
38
39impl TransportNativeHID {
40    fn is_ledger(dev: &DeviceInfo) -> bool {
41        dev.vendor_id() == LEDGER_VID && dev.usage_page() == LEDGER_USAGE_PAGE
42    }
43
44    /// Get a list of ledger devices available
45    pub fn list_ledgers(api: &HidApi) -> impl Iterator<Item = &DeviceInfo> {
46        api.device_list()
47            .filter(|dev| Self::is_ledger(dev))
48    }
49
50    /// Create a new HID transport, connecting to the first ledger found
51    /// # Warning
52    /// Opening the same device concurrently will lead to device lock after the first handle is closed
53    /// see [issue](https://github.com/ruabmbua/hidapi-rs/issues/81)
54    pub fn new(api: &HidApi) -> Result<Self, LedgerHIDError> {
55        let first_ledger = Self::list_ledgers(api)
56            .next()
57            .ok_or(LedgerHIDError::DeviceNotFound)?;
58
59        Self::open_device(api, first_ledger)
60    }
61
62    /// Open a specific ledger device
63    ///
64    /// # Note
65    /// No checks are made to ensure the device is a ledger device
66    ///
67    /// # Warning
68    /// Opening the same device concurrently will lead to device lock after the first handle is closed
69    /// see [issue](https://github.com/ruabmbua/hidapi-rs/issues/81)
70    pub fn open_device(
71        api: &HidApi,
72        device: &DeviceInfo,
73    ) -> Result<Self, LedgerHIDError> {
74        let device = device.open_device(api)?;
75        let _ = device.set_blocking_mode(true);
76
77        let ledger = TransportNativeHID { device: Mutex::new(device) };
78
79        Ok(ledger)
80    }
81
82    fn write_apdu(
83        device: &HidDevice,
84        channel: u16,
85        apdu_command: &[u8],
86    ) -> Result<i32, LedgerHIDError> {
87        let command_length = apdu_command.len();
88        let mut in_data = Vec::with_capacity(command_length + 2);
89        in_data.push(((command_length >> 8) & 0xFF) as u8);
90        in_data.push((command_length & 0xFF) as u8);
91        in_data.extend_from_slice(apdu_command);
92
93        let mut buffer = vec![0u8; LEDGER_PACKET_WRITE_SIZE as usize];
94        // Windows platform requires 0x00 prefix and Linux/Mac tolerate this as well
95        buffer[0] = 0x00;
96        buffer[1] = ((channel >> 8) & 0xFF) as u8; // channel big endian
97        buffer[2] = (channel & 0xFF) as u8; // channel big endian
98        buffer[3] = 0x05u8;
99
100        for (sequence_idx, chunk) in in_data
101            .chunks((LEDGER_PACKET_WRITE_SIZE - 6) as usize)
102            .enumerate()
103        {
104            buffer[4] = ((sequence_idx >> 8) & 0xFF) as u8; // sequence_idx big endian
105            buffer[5] = (sequence_idx & 0xFF) as u8; // sequence_idx big endian
106            buffer[6 .. 6 + chunk.len()].copy_from_slice(chunk);
107
108            info!("[{:3}] << {:}", buffer.len(), hex::encode(&buffer));
109
110            let result = device.write(&buffer);
111
112            match result {
113                Ok(size) => {
114                    if size < buffer.len() {
115                        return Err(LedgerHIDError::Comm("USB write error. Could not send whole message"));
116                    }
117                },
118                Err(x) => return Err(LedgerHIDError::Hid(x)),
119            }
120        }
121        Ok(1)
122    }
123
124    fn read_apdu(
125        device: &HidDevice,
126        channel: u16,
127        apdu_answer: &mut Vec<u8>,
128    ) -> Result<usize, LedgerHIDError> {
129        let mut buffer = vec![0u8; LEDGER_PACKET_READ_SIZE as usize];
130        let mut sequence_idx = 0u16;
131        let mut expected_apdu_len = 0usize;
132
133        loop {
134            let res = device.read_timeout(&mut buffer, LEDGER_TIMEOUT)?;
135
136            if (sequence_idx == 0 && res < 7) || res < 5 {
137                return Err(LedgerHIDError::Comm("Read error. Incomplete header"));
138            }
139
140            let mut rdr = Cursor::new(&buffer);
141
142            let rcv_channel = rdr.read_u16::<BigEndian>()?;
143            let rcv_tag = rdr.read_u8()?;
144            let rcv_seq_idx = rdr.read_u16::<BigEndian>()?;
145
146            if rcv_channel != channel {
147                return Err(LedgerHIDError::Comm("Invalid channel"));
148            }
149            if rcv_tag != 0x05u8 {
150                return Err(LedgerHIDError::Comm("Invalid tag"));
151            }
152
153            if rcv_seq_idx != sequence_idx {
154                return Err(LedgerHIDError::Comm("Invalid sequence idx"));
155            }
156
157            if rcv_seq_idx == 0 {
158                expected_apdu_len = rdr.read_u16::<BigEndian>()? as usize;
159            }
160
161            let available: usize = buffer.len() - rdr.position() as usize;
162            let missing: usize = expected_apdu_len - apdu_answer.len();
163            let end_p = rdr.position() as usize + std::cmp::min(available, missing);
164
165            let new_chunk = &buffer[rdr.position() as usize .. end_p];
166
167            info!("[{:3}] << {:}", new_chunk.len(), hex::encode(new_chunk));
168
169            apdu_answer.extend_from_slice(new_chunk);
170
171            if apdu_answer.len() >= expected_apdu_len {
172                return Ok(apdu_answer.len());
173            }
174
175            sequence_idx += 1;
176        }
177    }
178
179    pub fn exchange<I: Deref<Target = [u8]>>(
180        &self,
181        command: &APDUCommand<I>,
182    ) -> Result<APDUAnswer<Vec<u8>>, LedgerHIDError> {
183        let device = self
184            .device
185            .lock()
186            .expect("HID device poisoned");
187
188        Self::write_apdu(&device, LEDGER_CHANNEL, &command.serialize())?;
189
190        let mut answer: Vec<u8> = Vec::with_capacity(256);
191        Self::read_apdu(&device, LEDGER_CHANNEL, &mut answer)?;
192
193        APDUAnswer::from_answer(answer).map_err(|_| LedgerHIDError::Comm("response was too short"))
194    }
195}
196
197#[async_trait]
198impl Exchange for TransportNativeHID {
199    type Error = LedgerHIDError;
200    type AnswerType = Vec<u8>;
201
202    async fn exchange<I>(
203        &self,
204        command: &APDUCommand<I>,
205    ) -> Result<APDUAnswer<Self::AnswerType>, Self::Error>
206    where
207        I: Deref<Target = [u8]> + Send + Sync,
208    {
209        self.exchange(command)
210    }
211}
212
213#[cfg(test)]
214mod integration_tests {
215    use hidapi::HidApi;
216    use log::info;
217    use once_cell::sync::Lazy;
218    use serial_test::serial;
219
220    use crate::{APDUCommand, TransportNativeHID};
221
222    fn init_logging() {
223        let _ = env_logger::builder()
224            .is_test(true)
225            .try_init();
226    }
227
228    fn hidapi() -> &'static HidApi {
229        static HIDAPI: Lazy<HidApi> = Lazy::new(|| HidApi::new().expect("unable to get HIDAPI"));
230
231        &HIDAPI
232    }
233
234    #[test]
235    #[serial]
236    fn list_all_devices() {
237        init_logging();
238        let api = hidapi();
239
240        for device_info in api.device_list() {
241            info!(
242                "{:#?} - {:#x}/{:#x}/{:#x}/{:#x} {:#} {:#}",
243                device_info.path(),
244                device_info.vendor_id(),
245                device_info.product_id(),
246                device_info.usage_page(),
247                device_info.interface_number(),
248                device_info
249                    .manufacturer_string()
250                    .unwrap_or_default(),
251                device_info
252                    .product_string()
253                    .unwrap_or_default()
254            );
255        }
256    }
257
258    #[test]
259    #[serial]
260    fn ledger_device_path() {
261        init_logging();
262        let api = hidapi();
263
264        let mut ledgers = TransportNativeHID::list_ledgers(api);
265
266        let a_ledger = ledgers
267            .next()
268            .expect("could not find any ledger device");
269        info!("{:?}", a_ledger.path());
270    }
271
272    #[test]
273    #[serial]
274    fn serialize() {
275        let data = vec![0, 0, 0, 1, 0, 0, 0, 1];
276
277        let command = APDUCommand { cla: 0x56, ins: 0x01, p1: 0x00, p2: 0x00, data };
278
279        let serialized_command = command.serialize();
280
281        let expected = vec![86, 1, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1];
282
283        assert_eq!(serialized_command, expected)
284    }
285
286    #[test]
287    #[serial]
288    fn exchange() {
289        use ledger_zondax_generic::{App, AppExt};
290        struct Dummy;
291        impl App for Dummy {
292            const CLA: u8 = 0;
293        }
294
295        init_logging();
296
297        let ledger = TransportNativeHID::new(hidapi()).expect("Could not get a device");
298
299        // use device info command that works in the dashboard
300        let result = futures::executor::block_on(Dummy::get_device_info(&ledger)).expect("Error during exchange");
301        info!("{:x?}", result);
302    }
303
304    #[test]
305    #[serial]
306    #[ignore] //see https://github.com/ruabmbua/hidapi-rs/issues/81
307    fn open_same_device_twice() {
308        use ledger_zondax_generic::{App, AppExt};
309        struct Dummy;
310        impl App for Dummy {
311            const CLA: u8 = 0;
312        }
313
314        init_logging();
315
316        let api = hidapi();
317        let ledger = TransportNativeHID::list_ledgers(api)
318            .next()
319            .expect("could not get a device");
320
321        let t1 = TransportNativeHID::open_device(api, ledger).expect("Could not open device");
322        let t2 = TransportNativeHID::open_device(api, ledger).expect("Could not open device");
323
324        // use device info command that works in the dashboard
325        let (r1, r2) = futures::executor::block_on(futures::future::join(
326            Dummy::get_device_info(&t1),
327            Dummy::get_device_info(&t2),
328        ));
329
330        let (r1, r2) = (r1.expect("error during exchange (t1)"), r2.expect("error during exchange (t2)"));
331
332        info!("r1: {:x?}", r1);
333        info!("r2: {:x?}", r2);
334
335        assert_eq!(r1, r2);
336    }
337}