coins_ledger/transports/native/
hid.rs

1//! Native HID APDU transport for Ledger Nano hardware wallets
2
3use crate::{
4    common::{APDUAnswer, APDUCommand},
5    errors::LedgerError,
6};
7
8use byteorder::{BigEndian, ReadBytesExt};
9use hidapi_rusb::{DeviceInfo, HidApi, HidDevice};
10use once_cell::sync::Lazy;
11use std::{
12    io::Cursor,
13    sync::{Mutex, MutexGuard},
14};
15
16use super::NativeTransportError;
17
18const LEDGER_VID: u16 = 0x2c97;
19#[cfg(not(target_os = "linux"))]
20const LEDGER_USAGE_PAGE: u16 = 0xFFA0;
21const LEDGER_CHANNEL: u16 = 0x0101;
22// for Windows compatability, we prepend the buffer with a 0x00
23// so the actual buffer is 64 bytes
24const LEDGER_PACKET_WRITE_SIZE: u8 = 65;
25const LEDGER_PACKET_READ_SIZE: u8 = 64;
26const LEDGER_TIMEOUT: i32 = 10_000_000;
27
28/// The HID API instance.
29pub static HIDAPI: Lazy<HidApi> =
30    Lazy::new(|| HidApi::new().expect("Failed to initialize HID API"));
31
32/// Native HID transport for Ledger Nano hardware wallets
33pub struct TransportNativeHID {
34    device: Mutex<HidDevice>,
35}
36
37impl std::fmt::Debug for TransportNativeHID {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("TransportNativeHID").finish()
40    }
41}
42
43#[cfg(not(target_os = "linux"))]
44fn is_ledger(dev: &DeviceInfo) -> bool {
45    dev.vendor_id() == LEDGER_VID && dev.usage_page() == LEDGER_USAGE_PAGE
46}
47
48#[cfg(target_os = "linux")]
49fn is_ledger(dev: &DeviceInfo) -> bool {
50    dev.vendor_id() == LEDGER_VID
51}
52
53/// Get a list of ledger devices available
54fn list_ledgers(api: &HidApi) -> impl Iterator<Item = &DeviceInfo> {
55    api.device_list().filter(|dev| is_ledger(dev))
56}
57
58#[tracing::instrument(skip_all, err)]
59fn first_ledger(api: &HidApi) -> Result<HidDevice, NativeTransportError> {
60    let device = list_ledgers(api)
61        .next()
62        .ok_or(NativeTransportError::DeviceNotFound)?;
63
64    open_device(api, device)
65}
66
67/// Read the 5-byte response header.
68fn read_response_header(rdr: &mut Cursor<&[u8]>) -> Result<(u16, u8, u16), NativeTransportError> {
69    let rcv_channel = rdr.read_u16::<BigEndian>()?;
70    let rcv_tag = rdr.read_u8()?;
71    let rcv_seq_idx = rdr.read_u16::<BigEndian>()?;
72    Ok((rcv_channel, rcv_tag, rcv_seq_idx))
73}
74
75fn write_apdu(
76    device: &mut MutexGuard<'_, HidDevice>,
77    channel: u16,
78    apdu_command: &[u8],
79) -> Result<(), NativeTransportError> {
80    tracing::debug!(apdu = %hex::encode(apdu_command), bytes = apdu_command.len(), "Writing APDU to device");
81
82    let command_length = apdu_command.len();
83
84    // TODO: allocation-free method
85    let mut in_data = Vec::with_capacity(command_length + 2);
86    in_data.push(((command_length >> 8) & 0xFF) as u8);
87    in_data.push((command_length & 0xFF) as u8);
88    in_data.extend_from_slice(apdu_command);
89
90    let mut buffer = [0u8; LEDGER_PACKET_WRITE_SIZE as usize];
91    // Windows platform requires 0x00 prefix and Linux/Mac tolerate this as
92    // well. So we leave buffer[0] as 0x00
93    buffer[1] = ((channel >> 8) & 0xFF) as u8; // channel big endian
94    buffer[2] = (channel & 0xFF) as u8; // channel big endian
95    buffer[3] = 0x05u8;
96
97    for (sequence_idx, chunk) in in_data
98        .chunks((LEDGER_PACKET_WRITE_SIZE - 6) as usize)
99        .enumerate()
100    {
101        buffer[4] = ((sequence_idx >> 8) & 0xFF) as u8; // sequence_idx big endian
102        buffer[5] = (sequence_idx & 0xFF) as u8; // sequence_idx big endian
103        buffer[6..6 + chunk.len()].copy_from_slice(chunk);
104
105        tracing::trace!(
106            buffer = hex::encode(buffer),
107            sequence_idx,
108            bytes = chunk.len(),
109            "Writing chunk to device",
110        );
111        let result = device.write(&buffer).map_err(NativeTransportError::Hid)?;
112        if result < buffer.len() {
113            return Err(NativeTransportError::Comm(
114                "USB write error. Could not send whole message",
115            ));
116        }
117    }
118    Ok(())
119}
120
121/// Read a response APDU from the ledger channel.
122fn read_response_apdu(
123    device: &mut MutexGuard<'_, HidDevice>,
124    _channel: u16,
125) -> Result<Vec<u8>, NativeTransportError> {
126    let mut response_buffer = [0u8; LEDGER_PACKET_READ_SIZE as usize];
127    let mut sequence_idx = 0u16;
128    let mut expected_response_len = 0usize;
129    let mut offset = 0;
130
131    let mut answer_buf = vec![];
132
133    loop {
134        let remaining = expected_response_len
135            .checked_sub(offset)
136            .unwrap_or_default();
137
138        tracing::trace!(
139            sequence_idx,
140            expected_response_len,
141            remaining,
142            answer_size = answer_buf.len(),
143            "Reading response from device.",
144        );
145
146        let res = device.read_timeout(&mut response_buffer, LEDGER_TIMEOUT)?;
147
148        // The first packet contains the response length as u16, successive
149        // packets do not.
150        if (sequence_idx == 0 && res < 7) || res < 5 {
151            return Err(NativeTransportError::Comm("Read error. Incomplete header"));
152        }
153
154        let mut rdr: Cursor<&[u8]> = Cursor::new(&response_buffer[..]);
155        let (_, _, rcv_seq_idx) = read_response_header(&mut rdr)?;
156
157        // Check sequence index. A mismatch means someone else read a packet.s
158        if rcv_seq_idx != sequence_idx {
159            return Err(NativeTransportError::SequenceMismatch {
160                got: rcv_seq_idx,
161                expected: sequence_idx,
162            });
163        }
164
165        // The header packet contains the number of bytes of response data
166        if rcv_seq_idx == 0 {
167            expected_response_len = rdr.read_u16::<BigEndian>()? as usize;
168            tracing::trace!(
169                expected_response_len,
170                "Received response length from device",
171            );
172        }
173
174        // Read either until the end of the buffer, or until we have read the
175        // expected response length
176        let remaining_in_buf = response_buffer.len() - rdr.position() as usize;
177        let missing = expected_response_len - offset;
178        let end_p = rdr.position() as usize + std::cmp::min(remaining_in_buf, missing);
179
180        let new_chunk = &response_buffer[rdr.position() as usize..end_p];
181
182        // Copy the response to the answer
183        answer_buf.extend(new_chunk);
184        offset += new_chunk.len();
185
186        if offset >= expected_response_len {
187            return Ok(answer_buf);
188        }
189
190        sequence_idx += 1;
191    }
192}
193
194/// Open a specific ledger device
195///
196/// # Note
197/// No checks are made to ensure the device is a ledger device
198///
199/// # Warning
200/// Opening the same device concurrently will lead to device lock after the first handle is closed
201/// see [issue](https://github.com/ruabmbua/hidapi-rs/issues/81)
202fn open_device(api: &HidApi, device: &DeviceInfo) -> Result<HidDevice, NativeTransportError> {
203    let device = device
204        .open_device(api)
205        .map_err(NativeTransportError::CantOpen)?;
206    let _ = device.set_blocking_mode(true);
207
208    Ok(device)
209}
210
211impl TransportNativeHID {
212    /// Instantiate from a device.
213    const fn from_device(device: HidDevice) -> Self {
214        Self {
215            device: Mutex::new(device),
216        }
217    }
218
219    /// Open all ledger devices.
220    pub fn open_all_devices() -> Result<Vec<Self>, NativeTransportError> {
221        let api = &HIDAPI;
222        let devices = list_ledgers(api)
223            .map(|dev| open_device(api, dev))
224            .collect::<Result<Vec<_>, _>>()?;
225
226        Ok(devices.into_iter().map(Self::from_device).collect())
227    }
228
229    /// Create a new HID transport, connecting to the first ledger found
230    ///
231    /// # Warning
232    /// Opening the same device concurrently will lead to device lock after the first handle is closed
233    /// see [issue](https://github.com/ruabmbua/hidapi-rs/issues/81)
234    pub fn new() -> Result<Self, NativeTransportError> {
235        let api = &HIDAPI;
236
237        #[cfg(target_os = "android")]
238        {
239            // Using runtime detection since it's impossible to statically target Termux.
240            let is_termux = match std::env::var("PREFIX") {
241                Ok(prefix_var) => prefix_var.contains("/com.termux/"),
242                Err(_) => false,
243            };
244
245            if is_termux {
246                // Termux uses a special environment vairable TERMUX_USB_FD for this
247                let usb_fd = std::env::var("TERMUX_USB_FD")
248                    .map_err(|_| NativeTransportError::InvalidTermuxUsbFd)?
249                    .parse::<i32>()
250                    .map_err(|_| NativeTransportError::InvalidTermuxUsbFd)?;
251                return Ok(api.wrap_sys_device(usb_fd, -1).map(Self::from_device)?);
252            }
253        }
254
255        first_ledger(api).map(Self::from_device)
256    }
257
258    /// Get manufacturer string. Returns None on error, or on no string.
259    pub fn get_manufacturer_string(&self) -> Option<String> {
260        let device = self.device.lock().unwrap();
261        device.get_manufacturer_string().unwrap_or_default()
262    }
263
264    /// Exchange an APDU with the device. The response data will be written to `answer_buf`, and a
265    /// `APDUAnswer` struct will be created with a reference to `answer_buf`.
266    ///
267    /// It is strongly recommended that you use the `APDUAnswer` api instead of reading the raw
268    /// answer_buf response.
269    ///
270    /// If the method errors, the buf may contain a partially written response. It is not advised
271    /// to read this.
272    pub fn exchange(&self, command: &APDUCommand) -> Result<APDUAnswer, LedgerError> {
273        let answer = {
274            let mut device = self.device.lock().unwrap();
275            write_apdu(&mut device, LEDGER_CHANNEL, &command.serialize())?;
276            read_response_apdu(&mut device, LEDGER_CHANNEL)?
277        };
278
279        let answer = APDUAnswer::from_answer(answer)?;
280
281        match answer.response_status() {
282            None => Ok(answer),
283            Some(response) => {
284                if response.is_success() {
285                    Ok(answer)
286                } else {
287                    Err(response.into())
288                }
289            }
290        }
291    }
292}
293
294/*******************************************************************************
295*   (c) 2018-2022 ZondaX GmbH
296*
297*  Licensed under the Apache License, Version 2.0 (the "License");
298*  you may not use this file except in compliance with the License.
299*  You may obtain a copy of the License at
300*
301*      http://www.apache.org/licenses/LICENSE-2.0
302*
303*  Unless required by applicable law or agreed to in writing, software
304*  distributed under the License is distributed on an "AS IS" BASIS,
305*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
306*  See the License for the specific language governing permissions and
307*  limitations under the License.
308********************************************************************************/