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}