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