Skip to main content

canokey_protocol/
apdu.rs

1//! Physical ISO 7816 APDU encoding and borrowed response parsing.
2//!
3//! These codecs do not apply firmware rules, send commands, or interpret applet errors.
4use crate::{Error, ErrorKind, Phase, SecretBytes};
5/// Raw SW1/SW2 status, preserved even when the applet does not recognize it.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct StatusWord(u16);
8impl StatusWord {
9    /// Wrap the big-endian numeric status (for example, `0x9000`).
10    pub const fn new(raw: u16) -> Self {
11        Self(raw)
12    }
13    /// Return SW1 in the high byte and SW2 in the low byte.
14    pub const fn raw(self) -> u16 {
15        self.0
16    }
17    /// Return true only for `9000`; continuation and warning statuses are not success.
18    pub const fn is_success(self) -> bool {
19        self.0 == 0x9000
20    }
21}
22/// Requested response-data length (Le), excluding SW1/SW2.
23///
24/// [`CommandApdu::encode`] validates the numeric range for the chosen encoding.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ExpectedLength {
27    /// Omit Le entirely; distinct from an encoded zero byte.
28    Absent,
29    /// Request 1..=256 bytes in short form or 1..=65536 in extended form.
30    /// The maximum value encodes as zero; it does not require that many response bytes.
31    Exact(u32),
32}
33/// Physical Lc/Le field widths; neither variant implies device support.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum ApduEncoding {
36    /// One-byte Lc/Le: at most 255 command-data bytes and Le up to 256.
37    Short,
38    /// Extended Lc/Le: at most 65535 command-data bytes and Le up to 65536.
39    Extended,
40}
41/// Four-byte command header, without Lc, data, or Le.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct ApduHeader {
44    /// Class byte, including any application-selected channel/chaining bits.
45    pub cla: u8,
46    /// Instruction byte.
47    pub ins: u8,
48    /// First instruction parameter.
49    pub p1: u8,
50    /// Second instruction parameter.
51    pub p2: u8,
52}
53impl ApduHeader {
54    /// Construct a header without interpreting instruction semantics.
55    pub const fn new(cla: u8, ins: u8, p1: u8, p2: u8) -> Self {
56        Self { cla, ins, p1, p2 }
57    }
58}
59/// Owned encoded command; Debug is redacted and its buffer is wiped on drop.
60#[derive(Clone)]
61pub struct CommandApdu {
62    bytes: SecretBytes,
63    header: ApduHeader,
64    data_start: usize,
65    data_len: usize,
66    encoding: ApduEncoding,
67}
68impl std::fmt::Debug for CommandApdu {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.write_str("CommandApdu([REDACTED])")
71    }
72}
73impl CommandApdu {
74    /// Encode one complete physical APDU, copying `data` into a protected buffer.
75    ///
76    /// This does not split a logical command or check channel/device capabilities.
77    /// Use [`crate::operation::conversation`] for chaining and continuation.
78    ///
79    /// # Errors
80    /// Returns [`ErrorKind::InvalidArgument`] when data or Le exceeds the encoding
81    /// range, or when `Exact(0)` is supplied.
82    ///
83    /// # Examples
84    /// ```
85    /// use canokey_protocol::{ApduEncoding, ApduHeader, CommandApdu, ExpectedLength};
86    /// let command = CommandApdu::encode(
87    ///     ApduHeader::new(0, 0xcb, 0x3f, 0xff),
88    ///     &[0x5c, 1, 0x7e], ExpectedLength::Exact(256), ApduEncoding::Short,
89    /// )?;
90    /// assert_eq!(command.as_bytes(), &[0, 0xcb, 0x3f, 0xff, 3, 0x5c, 1, 0x7e, 0]);
91    /// # Ok::<(), canokey_protocol::Error>(())
92    /// ```
93    pub fn encode(
94        header: ApduHeader,
95        data: &[u8],
96        le: ExpectedLength,
97        encoding: ApduEncoding,
98    ) -> Result<Self, Error> {
99        let max = match encoding {
100            ApduEncoding::Short => 256,
101            ApduEncoding::Extended => 65536,
102        };
103        if data.len() >= max || matches!(le, ExpectedLength::Exact(n) if n == 0 || n > max as u32) {
104            return Err(Error::new(ErrorKind::InvalidArgument));
105        }
106        let mut bytes = Vec::with_capacity(9 + data.len());
107        bytes.extend_from_slice(&[header.cla, header.ins, header.p1, header.p2]);
108        let extended = encoding == ApduEncoding::Extended;
109        if !data.is_empty() {
110            if extended {
111                bytes.push(0);
112                bytes.extend_from_slice(&(data.len() as u16).to_be_bytes());
113            } else {
114                bytes.push(data.len() as u8);
115            }
116        } else if extended && le != ExpectedLength::Absent {
117            bytes.push(0);
118        }
119        let data_start = bytes.len();
120        bytes.extend_from_slice(data);
121        if let ExpectedLength::Exact(n) = le {
122            if extended {
123                bytes.extend_from_slice(&(n as u16).to_be_bytes());
124            } else {
125                bytes.push(n as u8);
126            }
127        }
128        Ok(Self {
129            bytes: SecretBytes::new(bytes),
130            header,
131            data_start,
132            data_len: data.len(),
133            encoding,
134        })
135    }
136    /// Borrow the complete encoded APDU. The caller sends these bytes unchanged.
137    pub fn as_bytes(&self) -> &[u8] {
138        self.bytes.as_bytes()
139    }
140    pub(crate) fn corrected(&self, le: u32) -> Result<Self, Error> {
141        Self::encode(
142            self.header,
143            &self.as_bytes()[self.data_start..self.data_start + self.data_len],
144            ExpectedLength::Exact(le),
145            self.encoding,
146        )
147    }
148}
149/// Borrowed response data and status. Parsing does not imply command success.
150#[derive(Clone, Copy)]
151pub struct ResponseApdu<'a> {
152    data: &'a [u8],
153    status: StatusWord,
154}
155impl<'a> ResponseApdu<'a> {
156    /// Split a complete response into data and its final two status bytes.
157    ///
158    /// # Errors
159    /// Returns [`ErrorKind::InvalidResponse`] during parsing for fewer than two bytes.
160    /// No size limit is applied here; [`crate::Operation`] enforces exchange budgets.
161    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
162        if bytes.len() < 2 {
163            return Err(Error::new(ErrorKind::InvalidResponse).at(Phase::Parsing));
164        }
165        let n = bytes.len();
166        Ok(Self {
167            data: &bytes[..n - 2],
168            status: StatusWord::new(u16::from_be_bytes([bytes[n - 2], bytes[n - 1]])),
169        })
170    }
171    /// Borrow response data, excluding SW1/SW2, with the original input lifetime.
172    pub fn data(&self) -> &'a [u8] {
173        self.data
174    }
175    /// Return the raw final status without applet-specific interpretation.
176    pub fn status(&self) -> StatusWord {
177        self.status
178    }
179}