ledger_lib/
lib.rs

1//! A Ledger hardware wallet communication library
2//!
3//! [Device] provides a high-level API for exchanging APDUs with Ledger devices using the [ledger_proto] traits.
4//! This is suitable for extension with application-specific interface traits, and automatically
5//! implemented over [Exchange] for low-level byte exchange with devices.
6//!
7//! [LedgerProvider] and [LedgerHandle] provide a high-level tokio-compatible [Transport]
8//! for application integration, supporting connecting to and interacting with ledger devices.
9//!
10//! Low-level [Transport] implementations are provided for [USB/HID](transport::UsbTransport),
11//! [BLE](transport::BleTransport) and [TCP](transport::TcpTransport), with a [Generic](transport::GenericTransport)
12//! implementation providing a common interface over all enabled transports.
13//!
14//! Note that futures produced by async methods of [Transport] and [Device] are not `Send`, i.e. they
15//! can't be used with multi-threaded async executors. The reason is that [UsbTransport](transport::UsbTransport)
16//! and [UsbDevice](transport::UsbDevice) are not `Send`. This is a (probably redundant) precaution
17//! against potential quirks that might occur when the underlying `hidapi` objects change threads.\
18//! In a multi-threaded async environment use [LedgerProvider] and [LedgerHandle] instead.
19//!
20//! ## Examples
21//!
22//! ```no_run
23//! use ledger_lib::{LedgerProvider, Filters, Transport, Device, DEFAULT_TIMEOUT};
24//!
25//! #[tokio::main]
26//! async fn main() -> anyhow::Result<()> {
27//!     // Fetch provider handle
28//!     let mut provider = LedgerProvider::init().await;
29//!
30//!     // List available devices
31//!     let devices = provider.list(Filters::Any).await?;
32//!
33//!     // Check we have -a- device to connect to
34//!     if devices.is_empty() {
35//!         return Err(anyhow::anyhow!("No devices found"));
36//!     }
37//!
38//!     // Connect to the first device
39//!     let mut ledger = provider.connect(devices[0].clone()).await?;
40//!
41//!     // Request device information
42//!     let info = ledger.app_info(DEFAULT_TIMEOUT).await?;
43//!     println!("info: {info:?}");
44//!
45//!     Ok(())
46//! }
47//! ```
48
49use std::{future::Future, time::Duration};
50
51use tracing::debug;
52
53use ledger_proto::{
54    apdus::{ExitAppReq, RunAppReq},
55    GenericApdu, StatusCode,
56};
57
58pub mod info;
59pub use info::LedgerInfo;
60
61mod error;
62pub use error::Error;
63
64pub mod transport;
65pub use transport::Transport;
66
67mod provider;
68pub use provider::{LedgerHandle, LedgerProvider};
69
70mod device;
71pub use device::Device;
72
73/// Default timeout helper for use with [Device] and [Exchange]/[NonSendExchange]
74pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3);
75
76/// Device discovery filter
77#[derive(Copy, Clone, Debug, Default, PartialEq, strum::Display)]
78#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
79#[non_exhaustive]
80pub enum Filters {
81    /// List all devices available using supported transport
82    #[default]
83    Any,
84    /// List only HID devices
85    Hid,
86    /// List only TCP devices
87    Tcp,
88    /// List only BLE device
89    Ble,
90}
91
92/// [Exchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices.
93pub trait Exchange {
94    fn exchange(
95        &mut self,
96        command: &[u8],
97        timeout: Duration,
98    ) -> impl Future<Output = Result<Vec<u8>, Error>> + Send;
99}
100
101/// Blanket [Exchange] impl for mutable references
102impl<T: Exchange + Send> Exchange for &mut T {
103    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
104        <T as Exchange>::exchange(self, command, timeout).await
105    }
106}
107
108/// [NonSendExchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices.
109///
110/// It is the same as [Exchange], but it doesn't enforce the `Send` bound on the returned future.
111#[allow(async_fn_in_trait)]
112pub trait NonSendExchange {
113    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error>;
114}
115
116/// Blanket [NonSendExchange] impl for types that implement [Exchange].
117impl<T: Exchange> NonSendExchange for T {
118    async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
119        <T as Exchange>::exchange(self, command, timeout).await
120    }
121}
122
123/// Launch an application by name and return a device handle.
124///
125/// This checks whether an application is running, exits this if it
126/// is not the desired application, then launches the specified app
127/// by name.
128///
129/// Note that this function is only usable with a `Transport` whose associated `Device` type
130/// implements `Exchange` (e.g. `LedgerProvider`).
131///
132/// # WARNING
133/// Due to the constant re-enumeration of devices when changing app
134/// contexts, and the lack of reported serial numbers by ledger devices,
135/// this is not incredibly reliable. Use at your own risk.
136///
137pub async fn launch_app<T>(
138    mut t: T,
139    info: <T as Transport>::Info,
140    app_name: &str,
141    opts: &LaunchAppOpts,
142    timeout: Duration,
143) -> Result<<T as Transport>::Device, Error>
144where
145    T: Transport<Info = LedgerInfo, Filters = Filters> + Send,
146    <T as Transport>::Device: Exchange + Send,
147{
148    let mut buff = [0u8; 256];
149
150    debug!("Connecting to {info:?}");
151
152    // Connect to device and fetch the currently running application
153    let mut d = t.connect(info.clone()).await?;
154    let i = d.app_info(timeout).await?;
155
156    // Early-return if we're already running the correct app
157    if i.name == app_name {
158        debug!("Already running app {app_name}");
159        return Ok(d);
160    }
161
162    // Send an exit request to the running app
163    if i.name != "BOLOS" {
164        debug!("Exiting running app {}", i.name);
165
166        match d
167            .request::<GenericApdu>(ExitAppReq::new(), &mut buff, timeout)
168            .await
169        {
170            Ok(_) | Err(Error::Status(StatusCode::Ok)) => (),
171            Err(e) => return Err(e),
172        }
173
174        debug!("Exit complete, reconnecting to {info:?}");
175
176        // Close and re-connect to the device
177        drop(d);
178
179        tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
180
181        d = reconnect(&mut t, info.clone(), opts).await?;
182    }
183
184    // Send run request
185    for i in 0..10 {
186        debug!("Issuing run request ({i}/10)");
187
188        let resp = d
189            .request::<GenericApdu>(RunAppReq::new(app_name), &mut buff, timeout)
190            .await;
191
192        // Handle responses
193        match resp {
194            // Ok response or status, app opened
195            Ok(_) | Err(Error::Status(StatusCode::Ok)) => {
196                debug!("Run request complete, reconnecting to {info:?}");
197
198                // Re-connect to the device following app loading
199                drop(d);
200
201                tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
202
203                d = reconnect(&mut t, info.clone(), opts).await?;
204
205                return Ok(d);
206            }
207            // Empty response, pending reply
208            Err(Error::EmptyResponse) => tokio::time::sleep(Duration::from_secs(1)).await,
209            // Error response, something failed
210            Err(e) => return Err(e),
211        }
212    }
213
214    Err(Error::Timeout)
215}
216
217pub struct LaunchAppOpts {
218    /// Delay prior to attempting device re-connection in seconds.
219    ///
220    /// This delay is required to allow the OS to re-enumerate the HID
221    /// device.
222    pub reconnect_delay_s: usize,
223
224    /// Timeout for reconnect operations in seconds.
225    pub reconnect_timeout_s: usize,
226}
227
228impl Default for LaunchAppOpts {
229    fn default() -> Self {
230        Self {
231            reconnect_delay_s: 3,
232            reconnect_timeout_s: 10,
233        }
234    }
235}
236
237/// Helper to reconnect to devices
238async fn reconnect<T: Transport<Info = LedgerInfo, Filters = Filters>>(
239    mut t: T,
240    info: LedgerInfo,
241    opts: &LaunchAppOpts,
242) -> Result<<T as Transport>::Device, Error> {
243    let mut new_info = None;
244
245    // Build filter based on device connection type
246    let filters = Filters::from(info.kind());
247
248    debug!("Starting reconnect");
249
250    // Await device reconnection
251    for i in 0..opts.reconnect_timeout_s {
252        debug!("Listing devices ({i}/{})", opts.reconnect_timeout_s);
253
254        // List available devices
255        let devices = t.list(filters).await?;
256
257        // Look for matching device listing
258        // We can't use -paths- here because the VID changes on launch
259        // nor device serials, because these are always set to 1 (?!)
260        match devices
261            .iter()
262            .find(|i| i.model == info.model && i.kind() == info.kind())
263        {
264            Some(i) => {
265                new_info = Some(i.clone());
266                break;
267            }
268            None => tokio::time::sleep(Duration::from_secs(1)).await,
269        };
270    }
271
272    let new_info = match new_info {
273        Some(v) => v,
274        None => return Err(Error::Closed),
275    };
276
277    debug!("Device found, reconnecting!");
278
279    // Connect to device using new information object
280    let d = t.connect(new_info).await?;
281
282    // Return new device connection
283    Ok(d)
284}