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}