ledger_lib/
device.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! High-level Ledger [Device] abstraction for application development

use std::time::Duration;

use encdec::{EncDec, Encode};
use tracing::{debug, error};

use ledger_proto::{
    apdus::{AppInfoReq, AppInfoResp, DeviceInfoReq, DeviceInfoResp},
    ApduError, ApduReq, StatusCode,
};

use crate::{
    info::{AppInfo, DeviceInfo},
    Error, Exchange,
};

const APDU_BUFF_LEN: usize = 256;

/// [Device] provides a high-level interface exchanging APDU objects with implementers of [Exchange]
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
pub trait Device {
    /// Issue a request APDU, returning a reponse APDU
    async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>(
        &mut self,
        request: impl ApduReq<'a> + Send,
        buff: &'b mut [u8],
        timeout: Duration,
    ) -> Result<RESP, Error>;

    /// Fetch application information
    async fn app_info(&mut self, timeout: Duration) -> Result<AppInfo, Error> {
        let mut buff = [0u8; APDU_BUFF_LEN];

        let r = self
            .request::<AppInfoResp>(AppInfoReq {}, &mut buff[..], timeout)
            .await?;

        Ok(AppInfo {
            name: r.name.to_string(),
            version: r.version.to_string(),
            flags: r.flags,
        })
    }

    /// Fetch device information
    async fn device_info(&mut self, timeout: Duration) -> Result<DeviceInfo, Error> {
        let mut buff = [0u8; APDU_BUFF_LEN];

        let r = self
            .request::<DeviceInfoResp>(DeviceInfoReq {}, &mut buff[..], timeout)
            .await?;

        Ok(DeviceInfo {
            target_id: r.target_id,
            se_version: r.se_version.to_string(),
            mcu_version: r.mcu_version.to_string(),
            flags: r.flags.to_vec(),
        })
    }
}

/// Generic [Device] implementation for types supporting [Exchange]
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
impl<T: Exchange + Send> Device for T {
    /// Issue a request APDU to a device, encoding and decoding internally then returning a response APDU
    async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>(
        &mut self,
        req: impl ApduReq<'a> + Send,
        buff: &'b mut [u8],
        timeout: Duration,
    ) -> Result<RESP, Error> {
        debug!("TX: {req:?}");

        // Encode request
        let n = encode_request(req, buff)?;

        // Send request to device
        let resp_bytes = self.exchange(&buff[..n], timeout).await?;

        // Copy response back to buffer prior to decode
        // (these hijinks are required to allow devices to avoid ownership of APDU data)
        let n = resp_bytes.len();
        if n > buff.len() {
            error!(
                "Response length exceeds buffer length ({} > {})",
                n,
                buff.len()
            );
            return Err(ApduError::InvalidLength.into());
        }
        buff[..n].copy_from_slice(&resp_bytes[..]);

        // Handle error responses (2 bytes long, only a status)
        if n == 2 {
            // Return status code if matched, unknown otherwise
            let v = u16::from_be_bytes([resp_bytes[0], resp_bytes[1]]);
            match StatusCode::try_from(v) {
                Ok(c) => return Err(Error::Status(c)),
                Err(_) => return Err(Error::UnknownStatus(resp_bytes[0], resp_bytes[1])),
            }
        }

        // Decode response data - status bytes
        let (resp, _) = RESP::decode(&buff[..n - 2])?;

        debug!("RX: {resp:?}");

        // Return decode response
        Ok(resp)
    }
}

/// Helper to perform APDU request encoding including the header, length, and body
fn encode_request<'a, REQ: ApduReq<'a>>(req: REQ, buff: &mut [u8]) -> Result<usize, Error> {
    let mut index = 0;

    let data_len = req.encode_len()?;

    // Check buffer length is reasonable
    if buff.len() < 5 + data_len {
        return Err(ApduError::InvalidLength.into());
    }

    // Encode request object

    // First the header
    let h = req.header();
    index += h.encode(&mut buff[index..])?;

    // Then the data length
    if data_len > u8::MAX as usize {
        return Err(ApduError::InvalidLength.into());
    }
    buff[index] = data_len as u8;
    index += 1;

    // Then finally the data
    index += req.encode(&mut buff[index..])?;

    Ok(index)
}

#[cfg(test)]
mod tests {
    use ledger_proto::{apdus::AppInfoReq, ApduStatic};

    use super::encode_request;

    #[test]
    fn test_encode_requests() {
        let mut buff = [0u8; 256];

        let req = AppInfoReq {};
        let n = encode_request(req, &mut buff).unwrap();
        assert_eq!(n, 5);
        assert_eq!(
            &buff[..n],
            &[AppInfoReq::CLA, AppInfoReq::INS, 0x00, 0x00, 0x00]
        );
    }
}