ledger_lib/transport/
ble.rs

1//! Bluetooth Low Energy (BLE) transport
2
3use std::{fmt::Display, pin::Pin, time::Duration};
4
5use btleplug::{
6    api::{
7        BDAddr, Central as _, Characteristic, Manager as _, Peripheral, ScanFilter,
8        ValueNotification, WriteType,
9    },
10    platform::Manager,
11};
12use futures::{stream::StreamExt, Stream};
13use tracing::{debug, error, trace, warn};
14use uuid::{uuid, Uuid};
15
16use super::{Exchange, Transport};
17use crate::{
18    info::{ConnInfo, LedgerInfo, Model},
19    Error,
20};
21
22/// Transport for listing and connecting to BLE connected Ledger devices
23pub struct BleTransport {
24    manager: Manager,
25    peripherals: Vec<(LedgerInfo, btleplug::platform::Peripheral)>,
26}
27
28/// BLE specific device information
29#[derive(Clone, Debug, PartialEq)]
30pub struct BleInfo {
31    name: String,
32    addr: BDAddr,
33}
34
35impl Display for BleInfo {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}", self.name)
38    }
39}
40
41/// BLE connected ledger device
42pub struct BleDevice {
43    pub info: BleInfo,
44    mtu: u8,
45    p: btleplug::platform::Peripheral,
46    c_write: Characteristic,
47    c_read: Characteristic,
48}
49
50/// Bluetooth spec for ledger devices
51/// see: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts#L32
52#[derive(Clone, PartialEq, Debug)]
53struct BleSpec {
54    pub model: Model,
55    pub service_uuid: Uuid,
56    pub notify_uuid: Uuid,
57    pub write_uuid: Uuid,
58    pub write_cmd_uuid: Uuid,
59}
60
61/// Spec for types of bluetooth device
62const BLE_SPECS: &[BleSpec] = &[
63    BleSpec {
64        model: Model::NanoX,
65        service_uuid: uuid!("13d63400-2c97-0004-0000-4c6564676572"),
66        notify_uuid: uuid!("13d63400-2c97-0004-0001-4c6564676572"),
67        write_uuid: uuid!("13d63400-2c97-0004-0002-4c6564676572"),
68        write_cmd_uuid: uuid!("13d63400-2c97-0004-0003-4c6564676572"),
69    },
70    BleSpec {
71        model: Model::Stax,
72        service_uuid: uuid!("13d63400-2c97-6004-0000-4c6564676572"),
73        notify_uuid: uuid!("13d63400-2c97-6004-0001-4c6564676572"),
74        write_uuid: uuid!("13d63400-2c97-6004-0002-4c6564676572"),
75        write_cmd_uuid: uuid!("13d63400-2c97-6004-0003-4c6564676572"),
76    },
77];
78
79impl BleTransport {
80    pub async fn new() -> Result<Self, Error> {
81        // Setup connection manager
82        let manager = Manager::new().await?;
83
84        Ok(Self {
85            manager,
86            peripherals: vec![],
87        })
88    }
89
90    /// Helper to perform scan for available BLE devices, used in [list] and [connect].
91    async fn scan_internal(
92        &self,
93        duration: Duration,
94    ) -> Result<Vec<(LedgerInfo, btleplug::platform::Peripheral)>, Error> {
95        let mut matched = vec![];
96
97        // Grab adapter list
98        let adapters = self.manager.adapters().await?;
99
100        // TODO: load filters?
101        let f = ScanFilter { services: vec![] };
102
103        // Search using adapters
104        for adapter in adapters.iter() {
105            let info = adapter.adapter_info().await?;
106            debug!("Scan with adapter {info}");
107
108            // Start scan with adaptor
109            adapter.start_scan(f.clone()).await?;
110
111            tokio::time::sleep(duration).await;
112
113            // Fetch peripheral list
114            let mut peripherals = adapter.peripherals().await?;
115            if peripherals.is_empty() {
116                debug!("No peripherals found on adaptor {info}");
117                continue;
118            }
119
120            // Load peripheral information
121            for p in peripherals.drain(..) {
122                // Fetch peripheral properties
123                let (properties, _connected) = (p.properties().await?, p.is_connected().await?);
124
125                // Skip peripherals where we couldn't fetch properties
126                let properties = match properties {
127                    Some(v) => v,
128                    None => {
129                        debug!("Failed to fetch properties for peripheral: {p:?}");
130                        continue;
131                    }
132                };
133
134                // Skip peripherals without a local name (NanoX should report this)
135                let name = match &properties.local_name {
136                    Some(v) => v,
137                    None => continue,
138                };
139
140                debug!("Peripheral: {p:?} props: {properties:?}");
141
142                // Match on peripheral names
143                let model = if name.contains("Nano X") {
144                    Model::NanoX
145                } else if name.contains("Stax") {
146                    Model::Stax
147                } else {
148                    continue;
149                };
150
151                // Add to device list
152                matched.push((
153                    LedgerInfo {
154                        model: model.clone(),
155                        conn: BleInfo {
156                            name: name.clone(),
157                            addr: properties.address,
158                        }
159                        .into(),
160                    },
161                    p,
162                ));
163            }
164        }
165
166        Ok(matched)
167    }
168}
169
170/// [Transport] implementation for [BleTransport]
171#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
172impl Transport for BleTransport {
173    type Filters = ();
174    type Info = BleInfo;
175    type Device = BleDevice;
176
177    /// List BLE connected ledger devices
178    async fn list(&mut self, _filters: Self::Filters) -> Result<Vec<LedgerInfo>, Error> {
179        // Scan for available devices
180        let devices = self.scan_internal(Duration::from_millis(1000)).await?;
181
182        // Filter to return info list
183        let info: Vec<_> = devices.iter().map(|d| d.0.clone()).collect();
184
185        // Save listed devices for next connect
186        self.peripherals = devices;
187
188        Ok(info)
189    }
190
191    /// Connect to a specific ledger device
192    ///
193    /// Note: this _must_ follow a [Self::list] operation to match `info` with known peripherals
194    async fn connect(&mut self, info: Self::Info) -> Result<Self::Device, Error> {
195        // Match known peripherals using provided device info
196        let (d, p) = match self
197            .peripherals
198            .iter()
199            .find(|(d, _p)| d.conn == info.clone().into())
200        {
201            Some(v) => v,
202            None => {
203                warn!("No device found matching: {info:?}");
204                return Err(Error::NoDevices);
205            }
206        };
207        let i = match &d.conn {
208            ConnInfo::Ble(i) => i,
209            _ => unreachable!(),
210        };
211
212        let name = &i.name;
213
214        // Fetch properties
215        let properties = p.properties().await?;
216
217        // Connect to device and subscribe to characteristics
218        // Fetch specs for matched model (contains characteristic identifiers)
219        let specs = match BLE_SPECS.iter().find(|s| s.model == d.model) {
220            Some(v) => v,
221            None => {
222                warn!("No specs for model: {:?}", d.model);
223                return Err(Error::Unknown);
224            }
225        };
226
227        // If we're not connected, attempt to connect
228        if !p.is_connected().await? {
229            if let Err(e) = p.connect().await {
230                warn!("Failed to connect to {name}: {e:?}");
231                return Err(Error::Unknown);
232            }
233
234            if !p.is_connected().await? {
235                warn!("Not connected to {name}");
236                return Err(Error::Unknown);
237            }
238        }
239
240        debug!("peripheral {name}: {p:?} properties: {properties:?}");
241
242        // Then, grab available services and locate characteristics
243        p.discover_services().await?;
244
245        let characteristics = p.characteristics();
246
247        trace!("Characteristics: {characteristics:?}");
248
249        let c_write = characteristics.iter().find(|c| c.uuid == specs.write_uuid);
250        let c_read = characteristics.iter().find(|c| c.uuid == specs.notify_uuid);
251
252        let (c_write, c_read) = match (c_write, c_read) {
253            (Some(w), Some(r)) => (w, r),
254            _ => {
255                error!("Failed to match read and write characteristics for {name}");
256                return Err(Error::Unknown);
257            }
258        };
259
260        // Create device instance
261        let mut d = BleDevice {
262            info: info.clone(),
263            mtu: 23,
264            p: p.clone(),
265            c_write: c_write.clone(),
266            c_read: c_read.clone(),
267        };
268
269        // Request MTU (cmd 0x08, seq: 0x0000, len: 0x0000)
270        match d.fetch_mtu().await {
271            Ok(mtu) => d.mtu = mtu,
272            Err(e) => {
273                warn!("Failed to fetch MTU: {:?}", e);
274            }
275        }
276
277        debug!("using MTU: {}", d.mtu);
278
279        Ok(d)
280    }
281}
282
283const BLE_HEADER_LEN: usize = 3;
284
285impl BleDevice {
286    /// Helper to write commands as chunks based on device MTU
287    async fn write_command(&mut self, cmd: u8, payload: &[u8]) -> Result<(), Error> {
288        // Setup outgoing data (adds 2-byte big endian length prefix)
289        let mut data = Vec::with_capacity(payload.len() + 2);
290        data.extend_from_slice(&(payload.len() as u16).to_be_bytes()); // Data length
291        data.extend_from_slice(payload); // Data
292
293        debug!("TX cmd: 0x{cmd:02x} payload: {data:02x?}");
294
295        // Write APDU in chunks
296        for (i, c) in data.chunks(self.mtu as usize - BLE_HEADER_LEN).enumerate() {
297            // Setup chunk buffer
298            let mut buff = Vec::with_capacity(self.mtu as usize);
299            let cmd = match i == 0 {
300                true => cmd,
301                false => 0x03,
302            };
303
304            buff.push(cmd); // Command
305            buff.extend_from_slice(&(i as u16).to_be_bytes()); // Sequence ID
306            buff.extend_from_slice(c);
307
308            debug!("Write chunk {i}: {:02x?}", buff);
309
310            self.p
311                .write(&self.c_write, &buff, WriteType::WithResponse)
312                .await?;
313        }
314
315        Ok(())
316    }
317
318    /// Helper to read response packet from notification channel
319    async fn read_data(
320        &mut self,
321        mut notifications: Pin<Box<dyn Stream<Item = ValueNotification> + Send>>,
322    ) -> Result<Vec<u8>, Error> {
323        // Await first response
324        let v = match notifications.next().await {
325            Some(v) => v.value,
326            None => {
327                return Err(Error::Closed);
328            }
329        };
330
331        debug!("RX: {:02x?}", v);
332
333        // Check response length is reasonable
334        if v.len() < 5 {
335            error!("response too short");
336            return Err(Error::UnexpectedResponse);
337        } else if v[0] != 0x05 {
338            error!("unexpected response type: {:?}", v[0]);
339            return Err(Error::UnexpectedResponse);
340        }
341
342        // Read out full response length
343        let len = v[4] as usize;
344        if len == 0 {
345            return Err(Error::EmptyResponse);
346        }
347
348        trace!("Expecting response length: {}", len);
349
350        // Setup response buffer
351        let mut buff = Vec::with_capacity(len);
352        buff.extend_from_slice(&v[5..]);
353
354        // Read further responses
355        // TODO: check this is correct with larger packets
356        while buff.len() < len {
357            // Await response notification
358            let v = match notifications.next().await {
359                Some(v) => v.value,
360                None => {
361                    error!("Failed to fetch next chunk from peripheral");
362                    self.p.unsubscribe(&self.c_read).await?;
363                    return Err(Error::Closed);
364                }
365            };
366
367            debug!("RX: {v:02x?}");
368
369            // TODO: check sequence index?
370
371            // add received data to buffer
372            buff.extend_from_slice(&v[5..]);
373        }
374
375        Ok(buff)
376    }
377
378    /// Helper to fetch the available MTU from a bluetooth device
379    async fn fetch_mtu(&mut self) -> Result<u8, Error> {
380        // Setup read characteristic subscription
381        self.p.subscribe(&self.c_read).await?;
382        let mut n = self.p.notifications().await?;
383
384        // Write get mtu command
385        self.write_command(0x08, &[]).await?;
386
387        // Await MTU response
388        let mtu = match n.next().await {
389            Some(r) if r.value[0] == 0x08 && r.value.len() == 6 => {
390                debug!("RX: {:02x?}", r);
391                r.value[5]
392            }
393            Some(r) => {
394                warn!("Unexpected MTU response: {r:02x?}");
395                return Err(Error::Unknown);
396            }
397            None => {
398                warn!("Failed to request MTU");
399                return Err(Error::Unknown);
400            }
401        };
402
403        // Unsubscribe from characteristic
404        self.p.unsubscribe(&self.c_read).await?;
405
406        Ok(mtu)
407    }
408
409    pub(crate) async fn is_connected(&self) -> Result<bool, Error> {
410        let c = self.p.is_connected().await?;
411        Ok(c)
412    }
413}
414
415/// [Exchange] impl for BLE backed devices
416#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
417impl Exchange for BleDevice {
418    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
419        // Fetch notification channel for responses
420        self.p.subscribe(&self.c_read).await?;
421        let notifications = self.p.notifications().await?;
422
423        // Write command data
424        if let Err(e) = self.write_command(0x05, command).await {
425            self.p.unsubscribe(&self.c_read).await?;
426            return Err(e);
427        }
428
429        debug!("Await response");
430
431        // Wait for response
432        let buff = match tokio::time::timeout(timeout, self.read_data(notifications)).await {
433            Ok(Ok(v)) => v,
434            Ok(Err(e)) => {
435                self.p.unsubscribe(&self.c_read).await?;
436                return Err(e);
437            }
438            Err(e) => {
439                self.p.unsubscribe(&self.c_read).await?;
440                return Err(e.into());
441            }
442        };
443
444        Ok(buff)
445    }
446}