ledger_lib/
device.rs

1//! High-level Ledger [Device] abstraction for application development
2
3use std::time::Duration;
4
5use async_trait::async_trait;
6use encdec::{EncDec, Encode};
7use tracing::{debug, error};
8
9use ledger_proto::{
10    apdus::{
11        decode_app_data, AppData, AppInfoReq, AppInfoResp, AppListNextReq, AppListStartReq,
12        DeviceInfoReq, DeviceInfoResp,
13    },
14    ApduError, ApduReq, GenericApdu, StatusCode,
15};
16
17use crate::{
18    info::{AppInfo, DeviceInfo},
19    Error, Exchange,
20};
21
22const APDU_BUFF_LEN: usize = 256;
23
24// Note: replacing the `async_trait` macro below with the "modern" syntax, i.e.
25//  fn foo(...) -> impl Future<Output = ...> + Send {
26//      async move { ... }
27//  }
28// results in a bunch of errors "lifetime bound not satisfied ... note: this is a known limitation
29// that will be removed in the future (see issue #100013 for more information)".
30// This happens with Rust 1.91 and below, while 1.92 is able to compile the code.
31// So, `async_trait` serves as a workaround for this issue.
32
33/// [Device] provides a high-level interface exchanging APDU objects with implementers of [Exchange].
34#[async_trait]
35pub trait Device {
36    /// Issue a request APDU, returning a response APDU
37    async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>(
38        &mut self,
39        request: impl ApduReq<'a> + Send,
40        buff: &'b mut [u8],
41        timeout: Duration,
42    ) -> Result<RESP, Error>;
43
44    /// Fetch application information
45    async fn app_info(&mut self, timeout: Duration) -> Result<AppInfo, Error> {
46        let mut buff = [0u8; APDU_BUFF_LEN];
47
48        let r = self
49            .request::<AppInfoResp>(AppInfoReq {}, &mut buff[..], timeout)
50            .await?;
51
52        Ok(AppInfo {
53            name: r.name.to_string(),
54            version: r.version.to_string(),
55            flags: r.flags,
56        })
57    }
58
59    /// Fetch device information
60    async fn device_info(&mut self, timeout: Duration) -> Result<DeviceInfo, Error> {
61        let mut buff = [0u8; APDU_BUFF_LEN];
62
63        let r = self
64            .request::<DeviceInfoResp>(DeviceInfoReq {}, &mut buff[..], timeout)
65            .await?;
66
67        Ok(DeviceInfo {
68            target_id: r.target_id,
69            se_version: r.se_version.to_string(),
70            mcu_version: r.mcu_version.to_string(),
71            flags: r.flags.to_vec(),
72        })
73    }
74
75    /// Fetch list of installed apps
76    async fn app_list(&mut self, timeout: Duration) -> Result<Vec<AppData>, Error> {
77        let mut buff = [0u8; APDU_BUFF_LEN];
78
79        let mut app_data_list: Vec<AppData> = Default::default();
80
81        let mut start: bool = true;
82
83        loop {
84            let r = match start {
85                true => {
86                    self.request::<GenericApdu>(AppListStartReq {}, &mut buff[..], timeout)
87                        .await
88                }
89                false => {
90                    self.request::<GenericApdu>(AppListNextReq {}, &mut buff[..], timeout)
91                        .await
92                }
93            };
94
95            start = false;
96
97            match r {
98                Ok(apdu_output) => {
99                    let mut offset: usize = 1;
100                    while offset < apdu_output.data.len() - 2 {
101                        let data = decode_app_data(apdu_output.data.as_slice(), &mut offset)
102                            .map_err(Error::from)?;
103                        app_data_list.push(data);
104                    }
105                }
106                Err(Error::Status(StatusCode::Ok)) => {
107                    break;
108                }
109                Err(e) => {
110                    error!("Command failed: {e:?}");
111                    return Err(e);
112                }
113            }
114        }
115        Ok(app_data_list)
116    }
117}
118
119/// Generic [Device] implementation for types supporting [Exchange]
120#[async_trait]
121impl<T: Exchange + Send> Device for T {
122    /// Issue a request APDU to a device, encoding and decoding internally then returning a response APDU
123    async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>(
124        &mut self,
125        req: impl ApduReq<'a> + Send,
126        buff: &'b mut [u8],
127        timeout: Duration,
128    ) -> Result<RESP, Error> {
129        debug!("TX: {req:?}");
130
131        // Encode request
132        let n = encode_request(req, buff)?;
133
134        // Send request to device
135        let resp_bytes = self.exchange(&buff[..n], timeout).await?;
136
137        // Copy response back to buffer prior to decode
138        // (these hijinks are required to allow devices to avoid ownership of APDU data)
139        let n = resp_bytes.len();
140        if n > buff.len() {
141            error!(
142                "Response length exceeds buffer length ({} > {})",
143                n,
144                buff.len()
145            );
146            return Err(ApduError::InvalidLength.into());
147        }
148        buff[..n].copy_from_slice(&resp_bytes[..]);
149
150        // Handle error responses (2 bytes long, only a status)
151        if n == 2 {
152            // Return status code if matched, unknown otherwise
153            let v = u16::from_be_bytes([resp_bytes[0], resp_bytes[1]]);
154            match StatusCode::try_from(v) {
155                Ok(c) => return Err(Error::Status(c)),
156                Err(_) => return Err(Error::UnknownStatus(resp_bytes[0], resp_bytes[1])),
157            }
158        }
159
160        // Decode response data - status bytes
161        let (resp, _) = RESP::decode(&buff[..n - 2])?;
162
163        debug!("RX: {resp:?}");
164
165        // Return decode response
166        Ok(resp)
167    }
168}
169
170/// Helper to perform APDU request encoding including the header, length, and body
171fn encode_request<'a, REQ: ApduReq<'a>>(req: REQ, buff: &mut [u8]) -> Result<usize, Error> {
172    let mut index = 0;
173
174    let data_len = req.encode_len()?;
175
176    // Check buffer length is reasonable
177    if buff.len() < 5 + data_len {
178        return Err(ApduError::InvalidLength.into());
179    }
180
181    // Encode request object
182
183    // First the header
184    let h = req.header();
185    index += h.encode(&mut buff[index..])?;
186
187    // Then the data length
188    if data_len > u8::MAX as usize {
189        return Err(ApduError::InvalidLength.into());
190    }
191    buff[index] = data_len as u8;
192    index += 1;
193
194    // Then finally the data
195    index += req.encode(&mut buff[index..])?;
196
197    Ok(index)
198}
199
200#[cfg(test)]
201mod tests {
202    use ledger_proto::{apdus::AppInfoReq, ApduStatic};
203
204    use super::encode_request;
205
206    #[test]
207    fn test_encode_requests() {
208        let mut buff = [0u8; 256];
209
210        let req = AppInfoReq {};
211        let n = encode_request(req, &mut buff).unwrap();
212        assert_eq!(n, 5);
213        assert_eq!(
214            &buff[..n],
215            &[AppInfoReq::CLA, AppInfoReq::INS, 0x00, 0x00, 0x00]
216        );
217    }
218}