coins_ledger/transports/native/
hid.rs1use 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;
22const LEDGER_PACKET_WRITE_SIZE: u8 = 65;
25const LEDGER_PACKET_READ_SIZE: u8 = 64;
26const LEDGER_TIMEOUT: i32 = 10_000_000;
27
28pub static HIDAPI: Lazy<HidApi> =
30 Lazy::new(|| HidApi::new().expect("Failed to initialize HID API"));
31
32pub 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
53fn 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
67fn 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 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 buffer[1] = ((channel >> 8) & 0xFF) as u8; buffer[2] = (channel & 0xFF) as u8; 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; buffer[5] = (sequence_idx & 0xFF) as u8; 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
121fn 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 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 if rcv_seq_idx != sequence_idx {
159 return Err(NativeTransportError::SequenceMismatch {
160 got: rcv_seq_idx,
161 expected: sequence_idx,
162 });
163 }
164
165 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 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 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
194fn 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 const fn from_device(device: HidDevice) -> Self {
214 Self {
215 device: Mutex::new(device),
216 }
217 }
218
219 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 pub fn new() -> Result<Self, NativeTransportError> {
235 let api = &HIDAPI;
236
237 #[cfg(target_os = "android")]
238 {
239 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 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 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 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