Skip to main content

ledger_sdk_device_base/
lib.rs

1mod errors;
2use std::str;
3
4use async_trait::async_trait;
5pub use errors::*;
6use ledger_sdk_transport::{APDUAnswer, APDUCommand, APDUErrorCode, Exchange};
7use serde::{Deserialize, Serialize};
8
9// Ledger generic (non app-specific) APDU constants
10const INS_GET_VERSION: u8 = 0x00;
11const CLA_APP_INFO: u8 = 0xb0;
12const INS_APP_INFO: u8 = 0x01;
13const CLA_DEVICE_INFO: u8 = 0xe0;
14const INS_DEVICE_INFO: u8 = 0x01;
15const USER_MESSAGE_CHUNK_SIZE: usize = 250;
16
17pub enum ChunkPayloadType {
18    /// First chunk
19    Init = 0x00,
20    /// Append chunk
21    Add = 0x01,
22    /// Last chunk
23    Last = 0x02,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
27/// App Version
28pub struct Version {
29    /// Application Mode
30    #[serde(rename(serialize = "testMode"))]
31    pub mode: u8,
32    /// Version Major
33    pub major: u16,
34    /// Version Minor
35    pub minor: u16,
36    /// Version Patch
37    pub patch: u16,
38    /// Device is locked
39    pub locked: bool,
40    /// Target ID
41    pub target_id: [u8; 4],
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
45/// App Device Info
46pub struct DeviceInfo {
47    /// Target ID
48    #[serde(rename(serialize = "targetId"))]
49    pub target_id: [u8; 4],
50    /// Secure Element Version
51    #[serde(rename(serialize = "seVersion"))]
52    pub se_version: String,
53    /// Device Flag
54    pub flag: Vec<u8>,
55    /// MCU Version
56    #[serde(rename(serialize = "mcuVersion"))]
57    pub mcu_version: String,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
61/// App Information
62pub struct AppInfo {
63    /// Name of the application
64    #[serde(rename(serialize = "appName"))]
65    pub app_name: String,
66    /// App version
67    #[serde(rename(serialize = "appVersion"))]
68    pub app_version: String,
69    /// Flag length
70    #[serde(rename(serialize = "flagLen"))]
71    pub flag_len: u8,
72    /// Flag value
73    #[serde(rename(serialize = "flagsValue"))]
74    pub flags_value: u8,
75    /// Flag Recovery
76    #[serde(rename(serialize = "flagsRecovery"))]
77    pub flag_recovery: bool,
78    /// Flag Signed MCU code
79    #[serde(rename(serialize = "flagsSignedMCUCode"))]
80    pub flag_signed_mcu_code: bool,
81    /// Flag Onboarded
82    #[serde(rename(serialize = "flagsOnboarded"))]
83    pub flag_onboarded: bool,
84    /// Flag Pin Validated
85    #[serde(rename(serialize = "flagsPINValidated"))]
86    pub flag_pin_validated: bool,
87}
88
89/// Defines what we can consider an "App"
90pub trait App {
91    /// App's APDU CLA
92    const CLA: u8;
93}
94
95#[async_trait]
96pub trait AppExt<E>: App
97where
98    E: Exchange + Send + Sync,
99    E::Error: std::error::Error,
100{
101    /// Check APDU status word. Ok on 0x9000, otherwise map to SDK errors.
102    // Normalize common APDU status handling: Ok on 0x9000, map others to AppSpecific/Unknown
103    fn handle_response_error(
104        response: &APDUAnswer<E::AnswerType>,
105    ) -> Result<(), LedgerAppError<E::Error>> {
106        match response.error_code() {
107            Ok(APDUErrorCode::NoError) => Ok(()),
108            Ok(err) => Err(LedgerAppError::AppSpecific(err as _, err.description())),
109            Err(err) => Err(LedgerAppError::Unknown(err)),
110        }
111    }
112
113    /// Same as `handle_response_error`, but also requires non-empty payload (signature).
114    fn handle_response_error_signature(
115        response: &APDUAnswer<E::AnswerType>,
116    ) -> Result<(), LedgerAppError<E::Error>> {
117        match response.error_code() {
118            Ok(APDUErrorCode::NoError) if response.data().is_empty() => {
119                Err(LedgerAppError::NoSignature)
120            }
121            Ok(APDUErrorCode::NoError) => Ok(()),
122            Ok(err) => Err(LedgerAppError::AppSpecific(err as _, err.description())),
123            Err(err) => Err(LedgerAppError::AppSpecific(
124                err,
125                "[APDU_ERROR] Unknown".to_string(),
126            )),
127        }
128    }
129
130    /// Query device info (target_id, SE/MCU versions, flags) via BOLOS CLA/INS.
131    async fn get_device_info(transport: &E) -> Result<DeviceInfo, LedgerAppError<E::Error>> {
132        // Build APDU to query device info (BOLOS CLA/INS)
133        let command = APDUCommand {
134            cla: CLA_DEVICE_INFO,
135            ins: INS_DEVICE_INFO,
136            p1: 0x00,
137            p2: 0x00,
138            data: Vec::new(),
139        };
140
141        // Send APDU and ensure status is success
142        let response = transport.exchange(&command).await?;
143        match response.error_code() {
144            Ok(APDUErrorCode::NoError) => {}
145            Ok(err) => return Err(LedgerAppError::Unknown(err as _)),
146            Err(err) => return Err(LedgerAppError::Unknown(err)),
147        }
148
149        // Start parsing the BOLOS device info payload
150        let response_data = response.data();
151
152        // First 4 bytes: target_id
153        let target_id_slice = &response_data[0..4];
154        let mut idx = 4;
155
156        // Next: SE version (len + bytes)
157        let se_version_len: usize = response_data[idx] as usize;
158        idx += 1;
159        let se_version_bytes = &response_data[idx..(idx + se_version_len)];
160
161        idx += se_version_len;
162
163        // Flags: len + bytes
164        let flags_len: usize = response_data[idx] as usize;
165        idx += 1;
166        let flag = &response_data[idx..idx + flags_len];
167        idx += flags_len;
168
169        // MCU version: len + bytes (strip trailing NUL if present)
170        let mcu_version_len: usize = response_data[idx] as usize;
171        idx += 1;
172        let mut tmp = &response_data[idx..idx + mcu_version_len];
173        if tmp[mcu_version_len - 1] == 0 {
174            tmp = &response_data[idx..idx + mcu_version_len - 1];
175        }
176
177        // Copy target_id to fixed-size array
178        let mut target_id = [Default::default(); 4];
179        target_id.copy_from_slice(target_id_slice);
180
181        // Convert string slices, map UTF-8 errors to domain error
182        let se_version = str::from_utf8(se_version_bytes).map_err(|_e| LedgerAppError::Utf8)?;
183        let mcu_version = str::from_utf8(tmp).map_err(|_e| LedgerAppError::Utf8)?;
184
185        // Assemble strongly-typed device info
186        let device_info = DeviceInfo {
187            target_id,
188            se_version: se_version.to_string(),
189            flag: flag.to_vec(),
190            mcu_version: mcu_version.to_string(),
191        };
192
193        Ok(device_info)
194    }
195
196    /// Query current app info (name, version, flags) from the device.
197    async fn get_app_info(transport: &E) -> Result<AppInfo, LedgerAppError<E::Error>> {
198        let command = APDUCommand {
199            cla: CLA_APP_INFO,
200            ins: INS_APP_INFO,
201            p1: 0x00,
202            p2: 0x00,
203            data: Vec::new(),
204        };
205
206        let response = transport.exchange(&command).await?;
207        match response.error_code() {
208            Ok(APDUErrorCode::NoError) => {}
209            Ok(err) => return Err(LedgerAppError::AppSpecific(err as _, err.description())),
210            Err(err) => return Err(LedgerAppError::Unknown(err as _)),
211        }
212
213        let response_data = response.data();
214
215        if response_data[0] != 1 {
216            return Err(LedgerAppError::InvalidFormatID);
217        }
218
219        let app_name_len: usize = response_data[1] as usize;
220        let app_name_bytes = &response_data[2..app_name_len];
221
222        let mut idx = 2 + app_name_len;
223        let app_version_len: usize = response_data[idx] as usize;
224        idx += 1;
225        let app_version_bytes = &response_data[idx..idx + app_version_len];
226
227        idx += app_version_len;
228
229        let app_flags_len = response_data[idx];
230        idx += 1;
231        let flags_value = response_data[idx];
232
233        let app_name = str::from_utf8(app_name_bytes).map_err(|_e| LedgerAppError::Utf8)?;
234        let app_version = str::from_utf8(app_version_bytes).map_err(|_e| LedgerAppError::Utf8)?;
235
236        let app_info = AppInfo {
237            app_name: app_name.to_string(),
238            app_version: app_version.to_string(),
239            flag_len: app_flags_len,
240            flags_value,
241            flag_recovery: (flags_value & 1) != 0,
242            flag_signed_mcu_code: (flags_value & 2) != 0,
243            flag_onboarded: (flags_value & 4) != 0,
244            flag_pin_validated: (flags_value & 128) != 0,
245        };
246
247        Ok(app_info)
248    }
249
250    /// Query application version using the implementing app's CLA.
251    async fn get_version(transport: &E) -> Result<Version, LedgerAppError<E::Error>> {
252        let command = APDUCommand {
253            cla: Self::CLA,
254            ins: INS_GET_VERSION,
255            p1: 0x00,
256            p2: 0x00,
257            data: Vec::new(),
258        };
259
260        let response = transport.exchange(&command).await?;
261        match response.error_code() {
262            Ok(APDUErrorCode::NoError) => {}
263            Ok(err) => return Err(LedgerAppError::Unknown(err as _)),
264            Err(err) => return Err(LedgerAppError::Unknown(err)),
265        }
266
267        let response_data = response.data();
268
269        let version = match response_data.len() {
270            // single byte version numbers
271            4 => Version {
272                mode: response_data[0],
273                major: response_data[1] as u16,
274                minor: response_data[2] as u16,
275                patch: response_data[3] as u16,
276                locked: false,
277                target_id: [0, 0, 0, 0],
278            },
279            // double byte version numbers
280            7 => Version {
281                mode: response_data[0],
282                major: response_data[1] as u16 * 256 + response_data[2] as u16,
283                minor: response_data[3] as u16 * 256 + response_data[4] as u16,
284                patch: response_data[5] as u16 * 256 + response_data[6] as u16,
285                locked: false,
286                target_id: [0, 0, 0, 0],
287            },
288            // double byte version numbers + lock + target id
289            9 => Version {
290                mode: response_data[0],
291                major: response_data[1] as u16,
292                minor: response_data[2] as u16,
293                patch: response_data[3] as u16,
294                locked: response_data[4] != 0,
295                target_id: [
296                    response_data[5],
297                    response_data[6],
298                    response_data[7],
299                    response_data[8],
300                ],
301            },
302            // double byte version numbers + lock + target id
303            12 => Version {
304                mode: response_data[0],
305                major: response_data[1] as u16 * 256 + response_data[2] as u16,
306                minor: response_data[3] as u16 * 256 + response_data[4] as u16,
307                patch: response_data[5] as u16 * 256 + response_data[6] as u16,
308                locked: response_data[7] != 0,
309                target_id: [
310                    response_data[8],
311                    response_data[9],
312                    response_data[10],
313                    response_data[11],
314                ],
315            },
316            _ => return Err(LedgerAppError::InvalidVersion),
317        };
318        Ok(version)
319    }
320
321    /// Send a long message in chunks using Init/Add/Last framing on p1.
322    async fn send_chunks<I: std::ops::Deref<Target = [u8]> + Send + Sync>(
323        transport: &E,
324        command: APDUCommand<I>,
325        message: &[u8],
326    ) -> Result<APDUAnswer<E::AnswerType>, LedgerAppError<E::Error>> {
327        let chunks = message.chunks(USER_MESSAGE_CHUNK_SIZE);
328        match chunks.len() {
329            0 => return Err(LedgerAppError::InvalidEmptyMessage),
330            n if n > 255 => return Err(LedgerAppError::InvalidMessageSize),
331            _ => (),
332        }
333
334        if command.p1 != ChunkPayloadType::Init as u8 {
335            return Err(LedgerAppError::InvalidChunkPayloadType);
336        }
337
338        let mut response = transport.exchange(&command).await?;
339        Self::handle_response_error(&response)?;
340
341        // Send message chunks
342        let last_chunk_index = chunks.len() - 1;
343        for (packet_idx, chunk) in chunks.enumerate() {
344            let mut p1 = ChunkPayloadType::Add as u8;
345            if packet_idx == last_chunk_index {
346                p1 = ChunkPayloadType::Last as u8;
347            }
348
349            let command = APDUCommand {
350                cla: command.cla,
351                ins: command.ins,
352                p1,
353                p2: command.p2,
354                data: chunk.to_vec(),
355            };
356
357            response = transport.exchange(&command).await?;
358            Self::handle_response_error(&response)?;
359        }
360
361        Ok(response)
362    }
363}
364
365impl<T, E> AppExt<E> for T
366where
367    T: App,
368    E: Exchange + Send + Sync,
369    E::Error: std::error::Error,
370{
371}