1mod errors;
15use btleplug::api::{
16 Central, CharPropFlags, Characteristic, Manager, Peripheral, ScanFilter, WriteType,
17};
18pub use errors::LedgerBleError;
19
20use byteorder::BigEndian;
21
22use futures::StreamExt;
23use log::info;
24use uuid::Uuid;
25
26use std::collections::VecDeque;
27use std::ops::Deref;
28
29use ledger_transport::{async_trait, APDUAnswer, APDUCommand, Exchange};
30
31use btleplug::platform::{self, PeripheralId};
32use byteorder::WriteBytesExt;
33
34use std::time::Duration;
35use tokio::time;
36
37const NANO_X_UUID: Uuid = Uuid::from_fields(
39 0x9c332c11,
40 0xe85b,
41 0x3c29,
42 &[0x53, 0x6c, 0x50, 0xc4, 0xd3, 0x47, 0xed, 0x4d],
43);
44
45const MTU_COMMAND_TAG: u8 = 0x08;
46const APDU_COMMAND_TAG: u8 = 0x05;
47
48#[derive(Debug)]
49struct Request {
50 command_tag: u8,
51 payload: Vec<u8>,
52}
53
54#[derive(Debug)]
55struct InitialPacket {
56 command_tag: u8,
57 data_length: u16, payload: Vec<u8>,
59}
60
61impl InitialPacket {
62 fn serialize(self) -> Vec<u8> {
63 let mut buf = Vec::with_capacity(self.payload.len() + 5);
64 buf.write_u8(self.command_tag).unwrap(); buf.write_u16::<BigEndian>(0).unwrap(); buf.write_u16::<BigEndian>(self.data_length).unwrap(); buf.extend(self.payload); buf
69 }
70}
71
72impl TryFrom<Vec<u8>> for InitialPacket {
73 type Error = LedgerBleError;
74
75 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
76 if value.len() < 5 {
77 return Err(LedgerBleError::InvalidPacket);
78 }
79 let payload = if value.len() == 5 {
80 Vec::new()
81 } else {
82 value[5..].to_vec()
83 };
84 Ok(InitialPacket {
85 command_tag: value[0],
86 data_length: u16::from_be_bytes(value[3..5].try_into().unwrap()),
87 payload,
88 })
89 }
90}
91
92#[derive(Debug)]
93struct SubsequentPacket {
94 command_tag: u8,
95 _packet_sequence_index: u16, payload: Vec<u8>,
97}
98
99impl SubsequentPacket {
100 fn serialize(self) -> Vec<u8> {
101 let mut buf = Vec::with_capacity(self.payload.len() + 3);
102 buf.write_u8(self.command_tag).unwrap(); buf.write_u16::<BigEndian>(0).unwrap(); buf.extend(self.payload); buf
106 }
107}
108
109impl TryFrom<Vec<u8>> for SubsequentPacket {
110 type Error = LedgerBleError;
111
112 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
113 if value.len() < 3 {
114 return Err(LedgerBleError::InvalidPacket);
115 }
116 let payload = if value.len() == 3 {
117 Vec::new()
118 } else {
119 value[3..].to_vec()
120 };
121 Ok(SubsequentPacket {
122 command_tag: value[0],
123 _packet_sequence_index: u16::from_be_bytes(value[1..3].try_into().unwrap()),
124 payload,
125 })
126 }
127}
128
129#[derive(Debug)]
130pub struct TransportNativeBle {
131 device: platform::Peripheral,
132 write_characteristic: Characteristic,
133 notify_characteristic: Characteristic,
134}
135
136impl TransportNativeBle {
137 fn is_ledger(peripheral: &platform::Peripheral) -> bool {
138 peripheral.id() == PeripheralId::from(NANO_X_UUID)
139 }
140
141 pub async fn list_ledgers(
143 manager: &platform::Manager,
144 ) -> Result<Vec<platform::Peripheral>, LedgerBleError> {
145 let adapter_list = manager.adapters().await?;
146
147 for adapter in adapter_list.iter() {
148 adapter
149 .start_scan(ScanFilter::default())
150 .await
151 .expect("Can't scan BLE adapter for connected devices...");
152 time::sleep(Duration::from_secs(2)).await;
153 let peripherals = adapter.peripherals().await?;
154 if !peripherals.is_empty() {
155 return Ok(peripherals.into_iter().filter(Self::is_ledger).collect());
156 }
157 }
158 Ok(vec![])
159 }
160
161 pub async fn new(manager: &platform::Manager) -> Result<Vec<Self>, LedgerBleError> {
163 let ledgers = Self::list_ledgers(manager).await?;
164 if ledgers.is_empty() {
165 return Err(LedgerBleError::DeviceNotFound);
166 }
167
168 let mut output = Vec::new();
169 for ledger in ledgers {
170 if !ledger.is_connected().await? {
171 ledger.connect().await?;
172 }
173 ledger.discover_services().await?;
174
175 let write_characteristic = ledger
176 .characteristics()
177 .into_iter()
178 .find(|x| x.properties.contains(CharPropFlags::WRITE))
179 .unwrap();
180
181 let notify_characteristic = ledger
182 .characteristics()
183 .into_iter()
184 .find(|x| x.properties.contains(CharPropFlags::NOTIFY))
185 .unwrap();
186
187 output.push(TransportNativeBle {
188 device: ledger,
189 write_characteristic,
190 notify_characteristic,
191 });
192 }
193 Ok(output)
194 }
195
196 pub async fn connect(peripheral: platform::Peripheral) -> Result<Self, LedgerBleError> {
198 if !peripheral.is_connected().await? {
199 peripheral.connect().await?;
200 }
201 peripheral.discover_services().await?;
202
203 let write_characteristic = peripheral
204 .characteristics()
205 .into_iter()
206 .find(|x| x.properties.contains(CharPropFlags::WRITE))
207 .unwrap();
208
209 let notify_characteristic = peripheral
210 .characteristics()
211 .into_iter()
212 .find(|x| x.properties.contains(CharPropFlags::NOTIFY))
213 .unwrap();
214
215 Ok(TransportNativeBle {
216 device: peripheral,
217 write_characteristic,
218 notify_characteristic,
219 })
220 }
221
222 async fn write_request(&self, request: Request, mtu: u8) -> Result<(), LedgerBleError> {
223 let mut payloads = request
225 .payload
226 .chunks(mtu as usize)
227 .collect::<VecDeque<_>>();
228 let initial_packet = InitialPacket {
229 command_tag: request.command_tag,
230 data_length: request.payload.len() as u16,
231 payload: payloads.pop_front().unwrap_or_default().to_vec(),
232 };
233 let subsequent_packets = payloads
234 .iter()
235 .enumerate()
236 .map(|(i, payload)| SubsequentPacket {
237 command_tag: 0x03,
238 _packet_sequence_index: i as u16 + 1,
239 payload: payload.to_vec(),
240 })
241 .collect::<Vec<_>>();
242
243 self.device.subscribe(&self.notify_characteristic).await?;
245
246 info!("writing initial packet {:#?}", initial_packet);
248 self.device
249 .write(
250 &self.write_characteristic,
251 &initial_packet.serialize(),
252 WriteType::WithResponse,
253 )
254 .await?;
255
256 for packet in subsequent_packets {
258 info!("writing subsequent packet {:#?}", packet);
259 self.device
260 .write(
261 &self.write_characteristic,
262 &packet.serialize(),
263 WriteType::WithResponse,
264 )
265 .await?;
266 }
267
268 Ok(())
269 }
270
271 async fn read_response(&self) -> Result<Vec<u8>, LedgerBleError> {
272 let mut buffer = Vec::new();
273 let mut stream = self.device.notifications().await?;
274 let initial_packet: InitialPacket = stream.next().await.unwrap().value.try_into()?;
275 info!("reading initial packet {:#?}", initial_packet);
276 buffer.extend(initial_packet.payload);
277 while buffer.len() < initial_packet.data_length as usize {
278 let packet: SubsequentPacket = stream.next().await.unwrap().value.try_into()?;
279 info!("reading subsequent packet {:#?}", packet);
280 buffer.extend(packet.payload);
281 }
282 Ok(buffer)
283 }
284
285 async fn get_mtu(&self) -> Result<u8, LedgerBleError> {
286 let request = Request {
287 command_tag: MTU_COMMAND_TAG,
288 payload: Vec::new(),
289 };
290 self.write_request(request, 0xff).await?;
291 self.read_response().await.map(|x| x[0])
292 }
293
294 async fn write_apdu(&self, apdu_command: &[u8]) -> Result<(), LedgerBleError> {
295 let request = Request {
296 command_tag: APDU_COMMAND_TAG,
297 payload: apdu_command.to_vec(),
298 };
299 let mtu = Self::get_mtu(self).await?;
300 Self::write_request(self, request, mtu).await?;
301 Ok(())
302 }
303
304 pub async fn exchange<I: Deref<Target = [u8]>>(
305 &self,
306 command: &APDUCommand<I>,
307 ) -> Result<APDUAnswer<Vec<u8>>, LedgerBleError> {
308 self.write_apdu(&command.serialize()).await?;
309
310 let answer = self.read_response().await?;
311
312 APDUAnswer::from_answer(answer).map_err(|_| LedgerBleError::Comm("response was too short"))
313 }
314}
315
316#[async_trait]
317impl Exchange for TransportNativeBle {
318 type Error = LedgerBleError;
319 type AnswerType = Vec<u8>;
320
321 async fn exchange<I>(
322 &self,
323 command: &APDUCommand<I>,
324 ) -> Result<APDUAnswer<Self::AnswerType>, Self::Error>
325 where
326 I: Deref<Target = [u8]> + Send + Sync,
327 {
328 self.exchange(command).await
329 }
330}
331
332#[cfg(test)]
333mod integration_tests {
334 use crate::TransportNativeBle;
335 use btleplug::platform;
336 use byteorder::{LittleEndian, WriteBytesExt};
337 use ledger_transport::APDUCommand;
338 use log::{debug, info};
339
340 use serial_test::serial;
341
342 fn init_logging() {
343 let _ = env_logger::builder().is_test(true).try_init();
344 }
345
346 #[tokio::test]
347 #[serial]
348 async fn ledger_device_path() {
349 init_logging();
350 let manager = platform::Manager::new().await.unwrap();
351 let ledgers = TransportNativeBle::list_ledgers(&manager).await.unwrap();
352 for ledger in ledgers {
353 info!("{:#?}", ledger);
354 }
355 }
356
357 #[tokio::test]
358 #[serial]
359 async fn test_get_mtu() {
360 let manager = platform::Manager::new().await.unwrap();
361 let ledgers = TransportNativeBle::new(&manager).await.unwrap();
362 let mtu = ledgers[0].get_mtu().await.unwrap();
363 info!("mtu: {}", mtu);
364 }
365
366 #[tokio::test]
367 #[serial]
368 async fn test_get_version() {
369 let manager = platform::Manager::new().await.unwrap();
370 let ledger = TransportNativeBle::new(&manager)
371 .await
372 .unwrap()
373 .pop()
374 .unwrap();
375 let command = APDUCommand {
376 cla: 0x55,
377 ins: 0x00,
378 p1: 0x00,
379 p2: 0x00,
380 data: vec![],
381 };
382 let res = ledger.exchange(&command).await;
383
384 info!("res: {:#?}", res);
385 }
386
387 #[tokio::test]
388 #[serial]
389 async fn test_get_pub_key() {
390 let manager = platform::Manager::new().await.unwrap();
391 let ledger = TransportNativeBle::new(&manager)
392 .await
393 .unwrap()
394 .pop()
395 .unwrap();
396
397 let hrp = b"cosmos";
398 debug!("hrp: {:?}", hrp);
399
400 let mut get_addr_payload: Vec<u8> = Vec::new();
401 get_addr_payload.write_u8(hrp.len() as u8).unwrap(); get_addr_payload.extend(hrp); get_addr_payload
404 .write_u32::<LittleEndian>(44 + 0x80000000)
405 .unwrap();
406 get_addr_payload
407 .write_u32::<LittleEndian>(118 + 0x80000000)
408 .unwrap();
409 get_addr_payload.write_u32::<LittleEndian>(0).unwrap();
410 get_addr_payload.write_u32::<LittleEndian>(0).unwrap();
411 get_addr_payload.write_u32::<LittleEndian>(0).unwrap();
412 let command = APDUCommand {
413 cla: 0x55,
414 ins: 0x04,
415 p1: 0x00,
416 p2: 0x00,
417 data: get_addr_payload,
418 };
419 let res = ledger.exchange(&command).await;
420
421 info!("res: {:#?}", res);
422 }
423}