From a31ed91ebcbfd972a41d921af7893c16fb1417e0 Mon Sep 17 00:00:00 2001 From: yogh333 Date: Tue, 29 Aug 2023 15:38:02 +0200 Subject: [PATCH 1/6] list application installed on devices using unsecure instructions (requires user's consent) --- cli/src/main.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index f8fa445..a1ea36d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -79,11 +79,21 @@ pub enum Command { #[clap(long)] app_name: String, }, + /// List applications installed on device + ListApp, } #[derive(Clone, Debug, Default, PartialEq)] pub struct ApduData(Vec); +#[derive(Clone, Debug, Default, PartialEq)] +pub struct AppInfo { + flags: u32, + hash_code_data: [u8; 32], + hash: [u8; 32], + name: String, +} + impl FromStr for ApduData { type Err = hex::FromHexError; @@ -220,6 +230,87 @@ async fn main() -> anyhow::Result<()> { Err(e) => println!("Command failed: {e:?}"), } } + }, + Command::ListApp => { + let mut d = connect(&mut p, &devices, args.index).await?; + let mut app_list: Vec = vec![]; + + let mut flag: bool = true; + let mut start: bool = true; + + while flag { + let req = GenericApdu { + header: ApduHeader { + cla: 0xe0, + ins: { + match start { + true => 0xde, + false => 0xdf, + } + }, + p1: 0x00, + p2: 0x00, + }, + data: vec![], + }; + + start = false; + + let mut buff = [0u8; 256]; + let resp = d + .request::(req, &mut buff, args.timeout.into()) + .await; + + match resp { + Ok(apdu_output) => { + //println!("Response: {}", apdu_output.data.encode_hex::()); + + let mut offset: usize = 1; + while offset < apdu_output.data.len() - 2 { + offset += 1; + let mut app_info: AppInfo = Default::default(); + let bytes = + <[u8; 4]>::try_from(&apdu_output.data[offset..offset + 4]).unwrap(); + app_info.flags = u32::from_be_bytes(bytes); + offset += 4; + app_info + .hash_code_data + .copy_from_slice(&apdu_output.data[offset..offset + 32]); + offset += 32; + app_info + .hash + .copy_from_slice(&apdu_output.data[offset..offset + 32]); + offset += 32; + let name_len: usize = apdu_output.data[offset] as usize; + offset += 1; + app_info.name = String::from_utf8(Vec::from( + &apdu_output.data[offset..offset + name_len], + )) + .unwrap(); + offset += name_len; + + app_list.push(app_info); + } + } + Err(Error::Status(StatusCode::Ok)) => { + println!("flags, name, hash, hash_code:"); + for info in &app_list { + println!( + "{:08x}, {}, {}, {}", + info.flags, + info.name, + info.hash.encode_hex::(), + info.hash_code_data.encode_hex::() + ); + } + flag = false; + } + Err(e) => { + println!("Command failed: {e:?}"); + flag = false; + } + } + } } } Ok(()) From 2b76eebfe86a697e2148866acebbe2a597a18cdd Mon Sep 17 00:00:00 2001 From: yogh333 Date: Thu, 31 Aug 2023 12:42:44 +0200 Subject: [PATCH 2/6] moved list app feature as default method on Device --- cli/src/main.rs | 87 +++++-------------------------------- lib/src/device.rs | 49 ++++++++++++++++++++- proto/src/apdus/app_list.rs | 59 +++++++++++++++++++++++++ proto/src/apdus/mod.rs | 3 ++ 4 files changed, 119 insertions(+), 79 deletions(-) create mode 100644 proto/src/apdus/app_list.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index a1ea36d..5e38c3b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -233,83 +233,16 @@ async fn main() -> anyhow::Result<()> { }, Command::ListApp => { let mut d = connect(&mut p, &devices, args.index).await?; - let mut app_list: Vec = vec![]; - - let mut flag: bool = true; - let mut start: bool = true; - - while flag { - let req = GenericApdu { - header: ApduHeader { - cla: 0xe0, - ins: { - match start { - true => 0xde, - false => 0xdf, - } - }, - p1: 0x00, - p2: 0x00, - }, - data: vec![], - }; - - start = false; - - let mut buff = [0u8; 256]; - let resp = d - .request::(req, &mut buff, args.timeout.into()) - .await; - - match resp { - Ok(apdu_output) => { - //println!("Response: {}", apdu_output.data.encode_hex::()); - - let mut offset: usize = 1; - while offset < apdu_output.data.len() - 2 { - offset += 1; - let mut app_info: AppInfo = Default::default(); - let bytes = - <[u8; 4]>::try_from(&apdu_output.data[offset..offset + 4]).unwrap(); - app_info.flags = u32::from_be_bytes(bytes); - offset += 4; - app_info - .hash_code_data - .copy_from_slice(&apdu_output.data[offset..offset + 32]); - offset += 32; - app_info - .hash - .copy_from_slice(&apdu_output.data[offset..offset + 32]); - offset += 32; - let name_len: usize = apdu_output.data[offset] as usize; - offset += 1; - app_info.name = String::from_utf8(Vec::from( - &apdu_output.data[offset..offset + name_len], - )) - .unwrap(); - offset += name_len; - - app_list.push(app_info); - } - } - Err(Error::Status(StatusCode::Ok)) => { - println!("flags, name, hash, hash_code:"); - for info in &app_list { - println!( - "{:08x}, {}, {}, {}", - info.flags, - info.name, - info.hash.encode_hex::(), - info.hash_code_data.encode_hex::() - ); - } - flag = false; - } - Err(e) => { - println!("Command failed: {e:?}"); - flag = false; - } - } + let list = d.app_list(args.timeout.into()).await?; + println!("flags, name, hash, hash_code:"); + for info in &list { + println!( + "{:08x}, {}, {}, {}", + info.flags, + info.name, + info.hash.encode_hex::(), + info.hash_code_data.encode_hex::() + ); } } } diff --git a/lib/src/device.rs b/lib/src/device.rs index d1fba7e..2b778e5 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -6,8 +6,11 @@ use encdec::{EncDec, Encode}; use tracing::{debug, error}; use ledger_proto::{ - apdus::{AppInfoReq, AppInfoResp, DeviceInfoReq, DeviceInfoResp}, - ApduError, ApduReq, StatusCode, + apdus::{ + decode_app_data, AppData, AppInfoReq, AppInfoResp, AppListNextReq, AppListStartReq, + DeviceInfoReq, DeviceInfoResp, + }, + ApduError, ApduReq, GenericApdu, StatusCode, }; use crate::{ @@ -58,6 +61,48 @@ pub trait Device { flags: r.flags.to_vec(), }) } + + /// Fetch list of installed apps + async fn app_list(&mut self, timeout: Duration) -> Result, Error> { + let mut buff = [0u8; APDU_BUFF_LEN]; + + let mut app_data_list: Vec = Default::default(); + + let mut start: bool = true; + + loop { + let r = match start { + true => { + self.request::(AppListStartReq {}, &mut buff[..], timeout) + .await + } + false => { + self.request::(AppListNextReq {}, &mut buff[..], timeout) + .await + } + }; + + start = false; + + match r { + Ok(apdu_output) => { + let mut offset: usize = 1; + while offset < apdu_output.data.len() - 2 { + let data = decode_app_data(&apdu_output, &mut offset).unwrap(); + app_data_list.push(data); + } + } + Err(Error::Status(StatusCode::Ok)) => { + break; + } + Err(e) => { + println!("Command failed: {e:?}"); + return Err(e); + } + } + } + Ok(app_data_list) + } } /// Generic [Device] implementation for types supporting [Exchange] diff --git a/proto/src/apdus/app_list.rs b/proto/src/apdus/app_list.rs new file mode 100644 index 0000000..e2e7f6c --- /dev/null +++ b/proto/src/apdus/app_list.rs @@ -0,0 +1,59 @@ +use encdec::{Decode, Encode}; + +use crate::{ApduError, ApduStatic, GenericApdu}; + +/// App List Start APDU command +#[derive(Copy, Clone, PartialEq, Debug, Default, Encode, Decode)] +#[encdec(error = "ApduError")] +pub struct AppListStartReq {} + +/// App List Next APDU command +#[derive(Copy, Clone, PartialEq, Debug, Default, Encode, Decode)] +#[encdec(error = "ApduError")] +pub struct AppListNextReq {} + +impl ApduStatic for AppListStartReq { + /// Device info request APDU is class `0xe0` + const CLA: u8 = 0xe0; + + /// Device info request APDU is instruction `0x01` + const INS: u8 = 0xde; +} + +impl ApduStatic for AppListNextReq { + /// Device info request APDU is class `0xe0` + const CLA: u8 = 0xe0; + + /// Device info request APDU is instruction `0x01` + const INS: u8 = 0xdf; +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct AppData { + pub flags: u32, + pub hash_code_data: [u8; 32], + pub hash: [u8; 32], + pub name: String, +} + +pub fn decode_app_data(apdu: &GenericApdu, offset: &mut usize) -> Result { + *offset += 1; + let mut app_info: AppData = Default::default(); + let bytes = <[u8; 4]>::try_from(&apdu.data[*offset..*offset + 4]).unwrap(); + app_info.flags = u32::from_be_bytes(bytes); + *offset += 4; + app_info + .hash_code_data + .copy_from_slice(&apdu.data[*offset..*offset + 32]); + *offset += 32; + app_info + .hash + .copy_from_slice(&apdu.data[*offset..*offset + 32]); + *offset += 32; + let name_len: usize = apdu.data[*offset] as usize; + *offset += 1; + app_info.name = String::from_utf8(Vec::from(&apdu.data[*offset..*offset + name_len])).unwrap(); + *offset += name_len; + + Ok(app_info) +} diff --git a/proto/src/apdus/mod.rs b/proto/src/apdus/mod.rs index 7c20825..ffad908 100644 --- a/proto/src/apdus/mod.rs +++ b/proto/src/apdus/mod.rs @@ -11,3 +11,6 @@ pub use run_app::RunAppReq; mod exit_app; pub use exit_app::ExitAppReq; + +mod app_list; +pub use app_list::{decode_app_data, AppData, AppListNextReq, AppListStartReq}; From 9316d481892be06f3ea780dc3dcf6f39c2fc22e9 Mon Sep 17 00:00:00 2001 From: yogh333 Date: Fri, 1 Sep 2023 09:27:19 +0200 Subject: [PATCH 3/6] fix CI test issue --- proto/src/apdus/app_list.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proto/src/apdus/app_list.rs b/proto/src/apdus/app_list.rs index e2e7f6c..b8ca1ed 100644 --- a/proto/src/apdus/app_list.rs +++ b/proto/src/apdus/app_list.rs @@ -1,5 +1,8 @@ use encdec::{Decode, Encode}; +use alloc::string::String; +use alloc::vec::Vec; + use crate::{ApduError, ApduStatic, GenericApdu}; /// App List Start APDU command From 160db21228ef20539780259ae273e50f038cf006 Mon Sep 17 00:00:00 2001 From: yogh333 Date: Fri, 1 Sep 2023 09:48:46 +0200 Subject: [PATCH 4/6] fix CI test issue when no_std building of proto package --- lib/src/device.rs | 2 +- proto/src/apdus/app_list.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/src/device.rs b/lib/src/device.rs index 2b778e5..183c5b8 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -88,7 +88,7 @@ pub trait Device { Ok(apdu_output) => { let mut offset: usize = 1; while offset < apdu_output.data.len() - 2 { - let data = decode_app_data(&apdu_output, &mut offset).unwrap(); + let data = decode_app_data(apdu_output.data.as_slice(), &mut offset).unwrap(); app_data_list.push(data); } } diff --git a/proto/src/apdus/app_list.rs b/proto/src/apdus/app_list.rs index b8ca1ed..e81e547 100644 --- a/proto/src/apdus/app_list.rs +++ b/proto/src/apdus/app_list.rs @@ -1,9 +1,10 @@ use encdec::{Decode, Encode}; +extern crate alloc; use alloc::string::String; use alloc::vec::Vec; -use crate::{ApduError, ApduStatic, GenericApdu}; +use crate::{ApduError, ApduStatic}; /// App List Start APDU command #[derive(Copy, Clone, PartialEq, Debug, Default, Encode, Decode)] @@ -39,23 +40,23 @@ pub struct AppData { pub name: String, } -pub fn decode_app_data(apdu: &GenericApdu, offset: &mut usize) -> Result { +pub fn decode_app_data(data: &[u8], offset: &mut usize) -> Result { *offset += 1; let mut app_info: AppData = Default::default(); - let bytes = <[u8; 4]>::try_from(&apdu.data[*offset..*offset + 4]).unwrap(); + let bytes = <[u8; 4]>::try_from(&data[*offset..*offset + 4]).unwrap(); app_info.flags = u32::from_be_bytes(bytes); *offset += 4; app_info .hash_code_data - .copy_from_slice(&apdu.data[*offset..*offset + 32]); + .copy_from_slice(&data[*offset..*offset + 32]); *offset += 32; app_info .hash - .copy_from_slice(&apdu.data[*offset..*offset + 32]); + .copy_from_slice(&data[*offset..*offset + 32]); *offset += 32; - let name_len: usize = apdu.data[*offset] as usize; + let name_len: usize = data[*offset] as usize; *offset += 1; - app_info.name = String::from_utf8(Vec::from(&apdu.data[*offset..*offset + name_len])).unwrap(); + app_info.name = String::from_utf8(Vec::from(&data[*offset..*offset + name_len])).unwrap(); *offset += name_len; Ok(app_info) From d9034e5d94b36de4911865c3ea962c4722d607df Mon Sep 17 00:00:00 2001 From: yogh333 Date: Fri, 1 Sep 2023 09:52:30 +0200 Subject: [PATCH 5/6] cargo fmt + clippy --- cli/src/main.rs | 2 +- lib/src/device.rs | 3 ++- lib/src/lib.rs | 9 ++------- lib/src/transport/usb.rs | 5 +---- proto/src/apdus/app_list.rs | 4 +--- 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 5e38c3b..b4758dd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -230,7 +230,7 @@ async fn main() -> anyhow::Result<()> { Err(e) => println!("Command failed: {e:?}"), } } - }, + } Command::ListApp => { let mut d = connect(&mut p, &devices, args.index).await?; let list = d.app_list(args.timeout.into()).await?; diff --git a/lib/src/device.rs b/lib/src/device.rs index 183c5b8..0f86056 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -88,7 +88,8 @@ pub trait Device { Ok(apdu_output) => { let mut offset: usize = 1; while offset < apdu_output.data.len() - 2 { - let data = decode_app_data(apdu_output.data.as_slice(), &mut offset).unwrap(); + let data = + decode_app_data(apdu_output.data.as_slice(), &mut offset).unwrap(); app_data_list.push(data); } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index da27300..b9048c0 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -80,11 +80,12 @@ pub use device::Device; pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); /// Device discovery filter -#[derive(Copy, Clone, Debug, PartialEq, strum::Display)] +#[derive(Copy, Clone, Debug, Default, PartialEq, strum::Display)] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[non_exhaustive] pub enum Filters { /// List all devices available using supported transport + #[default] Any, /// List only HID devices Hid, @@ -94,12 +95,6 @@ pub enum Filters { Ble, } -impl Default for Filters { - fn default() -> Self { - Self::Any - } -} - /// [Exchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices #[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)] pub trait Exchange { diff --git a/lib/src/transport/usb.rs b/lib/src/transport/usb.rs index 783b056..ca36c15 100644 --- a/lib/src/transport/usb.rs +++ b/lib/src/transport/usb.rs @@ -256,10 +256,7 @@ impl UsbDevice { trace!("Read chunk {seq_idx} ({rem} bytes remaining)"); // Read next chunk, constant timeout as chunks should be sent end-to-end - let n = match self.device.read_timeout(&mut buff, 500) { - Ok(n) => n, - Err(e) => return Err(e.into()), - }; + let n = self.device.read_timeout(&mut buff, 500)?; if n < 5 { error!("Invalid chunk length {n}"); diff --git a/proto/src/apdus/app_list.rs b/proto/src/apdus/app_list.rs index e81e547..b336d37 100644 --- a/proto/src/apdus/app_list.rs +++ b/proto/src/apdus/app_list.rs @@ -50,9 +50,7 @@ pub fn decode_app_data(data: &[u8], offset: &mut usize) -> Result Date: Fri, 7 Nov 2025 15:04:42 +0100 Subject: [PATCH 6/6] Updates after Copilot review --- cli/src/main.rs | 8 ------- lib/src/device.rs | 6 ++--- proto/src/apdus/app_list.rs | 44 ++++++++++++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index b4758dd..ff9f6a7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -86,14 +86,6 @@ pub enum Command { #[derive(Clone, Debug, Default, PartialEq)] pub struct ApduData(Vec); -#[derive(Clone, Debug, Default, PartialEq)] -pub struct AppInfo { - flags: u32, - hash_code_data: [u8; 32], - hash: [u8; 32], - name: String, -} - impl FromStr for ApduData { type Err = hex::FromHexError; diff --git a/lib/src/device.rs b/lib/src/device.rs index 0f86056..8315e27 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -88,8 +88,8 @@ pub trait Device { Ok(apdu_output) => { let mut offset: usize = 1; while offset < apdu_output.data.len() - 2 { - let data = - decode_app_data(apdu_output.data.as_slice(), &mut offset).unwrap(); + let data = decode_app_data(apdu_output.data.as_slice(), &mut offset) + .map_err(Error::from)?; app_data_list.push(data); } } @@ -97,7 +97,7 @@ pub trait Device { break; } Err(e) => { - println!("Command failed: {e:?}"); + error!("Command failed: {e:?}"); return Err(e); } } diff --git a/proto/src/apdus/app_list.rs b/proto/src/apdus/app_list.rs index b336d37..0242bbb 100644 --- a/proto/src/apdus/app_list.rs +++ b/proto/src/apdus/app_list.rs @@ -17,18 +17,18 @@ pub struct AppListStartReq {} pub struct AppListNextReq {} impl ApduStatic for AppListStartReq { - /// Device info request APDU is class `0xe0` + /// App list start request APDU is class `0xe0` const CLA: u8 = 0xe0; - /// Device info request APDU is instruction `0x01` + /// App list start request APDU is instruction `0x01` const INS: u8 = 0xde; } impl ApduStatic for AppListNextReq { - /// Device info request APDU is class `0xe0` + /// App list next request APDU is class `0xe0` const CLA: u8 = 0xe0; - /// Device info request APDU is instruction `0x01` + /// App list next request APDU is instruction `0x01` const INS: u8 = 0xdf; } @@ -40,20 +40,54 @@ pub struct AppData { pub name: String, } +/// Decodes an `AppData` structure from a binary slice, starting at the given offset. +/// +/// # Binary format +/// The expected format of the data is as follows (all fields are in order): +/// +/// - [0] : 1 byte - Reserved or tag byte (skipped by `*offset += 1`) +/// - [1..5] : 4 bytes - Flags (big-endian u32) +/// - [5..37] : 32 bytes - Hash code data +/// - [37..69] : 32 bytes - Hash +/// - [69] : 1 byte - Length of the name field (N) +/// - [70..70+N]: N bytes - Name (UTF-8 encoded) +/// +/// The function updates the provided `offset` as it parses each field. +/// +/// # Arguments +/// * `data` - The binary slice containing the encoded `AppData`. +/// * `offset` - A mutable reference to the current offset in the slice. This will be updated as fields are parsed. +/// +/// # Returns +/// * `Ok(AppData)` if decoding is successful. +/// * `Err(ApduError)` if decoding fails. pub fn decode_app_data(data: &[u8], offset: &mut usize) -> Result { *offset += 1; let mut app_info: AppData = Default::default(); - let bytes = <[u8; 4]>::try_from(&data[*offset..*offset + 4]).unwrap(); + let bytes = + <[u8; 4]>::try_from(&data[*offset..*offset + 4]).map_err(|_| ApduError::InvalidLength)?; app_info.flags = u32::from_be_bytes(bytes); *offset += 4; + if data.len() < *offset + 32 { + return Err(ApduError::InvalidLength); + } app_info .hash_code_data .copy_from_slice(&data[*offset..*offset + 32]); *offset += 32; + if data.len() < *offset + 32 { + return Err(ApduError::InvalidLength); + } app_info.hash.copy_from_slice(&data[*offset..*offset + 32]); *offset += 32; + if data.len() <= *offset { + return Err(ApduError::InvalidLength); + } let name_len: usize = data[*offset] as usize; *offset += 1; + if data.len() < *offset + name_len { + return Err(ApduError::InvalidLength); + } app_info.name = String::from_utf8(Vec::from(&data[*offset..*offset + name_len])).unwrap(); *offset += name_len;