use std::fmt;
use crate::error::{Error, Result};
pub mod cla {
pub const USER: u8 = 0x00;
pub const SYSTEM: u8 = 0x80;
pub const SM_WITHOUT_INTEGRITY: u8 = 0x08;
pub const SM_WITH_INTEGRITY: u8 = 0x0C;
pub const fn with_channel(cla: u8, channel: u8) -> u8 {
(cla & 0xFC) | (channel & 0x03)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command {
pub cla: u8,
pub ins: u8,
pub p1: u8,
pub p2: u8,
pub data: Vec<u8>,
pub le: Option<u32>,
}
impl Command {
pub fn new(cla: u8, ins: u8, p1: u8, p2: u8) -> Self {
Command {
cla,
ins,
p1,
p2,
data: Vec::new(),
le: None,
}
}
pub fn with_le(cla: u8, ins: u8, p1: u8, p2: u8, le: u32) -> Self {
Command {
cla,
ins,
p1,
p2,
data: Vec::new(),
le: Some(le),
}
}
pub fn with_data(cla: u8, ins: u8, p1: u8, p2: u8, data: impl Into<Vec<u8>>) -> Self {
Command {
cla,
ins,
p1,
p2,
data: data.into(),
le: None,
}
}
pub fn with_data_le(
cla: u8,
ins: u8,
p1: u8,
p2: u8,
data: impl Into<Vec<u8>>,
le: u32,
) -> Self {
Command {
cla,
ins,
p1,
p2,
data: data.into(),
le: Some(le),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
if self.data.len() > 0xFFFF {
return Err(Error::DataTooLong(self.data.len()));
}
if let Some(le) = self.le {
if le == 0 || le > 65536 {
return Err(Error::ExpectedLengthOutOfRange(le));
}
}
let extended = self.data.len() > 255 || self.le.is_some_and(|le| le > 256);
let mut out = Vec::with_capacity(7 + self.data.len() + 2);
out.extend_from_slice(&[self.cla, self.ins, self.p1, self.p2]);
if !self.data.is_empty() {
if extended {
out.push(0x00);
out.extend_from_slice(&(self.data.len() as u16).to_be_bytes());
} else {
out.push(self.data.len() as u8);
}
out.extend_from_slice(&self.data);
}
if let Some(le) = self.le {
if extended {
if self.data.is_empty() {
out.push(0x00);
}
out.extend_from_slice(&(le as u16).to_be_bytes());
} else {
out.push(le as u8);
}
}
Ok(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub data: Vec<u8>,
pub status: StatusWord,
}
impl Response {
pub fn parse(raw: &[u8]) -> Result<Self> {
if raw.len() < 2 {
return Err(Error::ShortResponse(raw.len()));
}
let (data, sw) = raw.split_at(raw.len() - 2);
Ok(Response {
data: data.to_vec(),
status: StatusWord::new(u16::from_be_bytes([sw[0], sw[1]])),
})
}
pub fn into_data(self) -> Result<Vec<u8>> {
if self.status.is_success() {
Ok(self.data)
} else {
Err(Error::from_status(self.status))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StatusWord(u16);
impl StatusWord {
pub const SUCCESS: StatusWord = StatusWord(0x9000);
pub const fn new(value: u16) -> Self {
StatusWord(value)
}
pub const fn value(self) -> u16 {
self.0
}
pub const fn sw1(self) -> u8 {
(self.0 >> 8) as u8
}
pub const fn sw2(self) -> u8 {
self.0 as u8
}
pub const fn is_success(self) -> bool {
self.0 == 0x9000
}
pub const fn is_warning(self) -> bool {
matches!(self.sw1(), 0x62 | 0x63)
}
pub const fn more_data_available(self) -> Option<u8> {
if self.sw1() == 0x61 {
Some(self.sw2())
} else {
None
}
}
pub const fn correct_le(self) -> Option<u8> {
if self.sw1() == 0x6C {
Some(self.sw2())
} else {
None
}
}
pub const fn retries_remaining(self) -> Option<u8> {
if self.0 & 0xFFF0 == 0x63C0 {
Some(self.sw2() & 0x0F)
} else {
None
}
}
pub const fn is_unlimited_retry(self) -> bool {
self.0 == 0x6300
}
pub const fn description(self) -> Option<&'static str> {
Some(match self.0 {
0x9000 => "normal end",
0x6281 => "output data failure",
0x6283 => "DF locked",
0x6300 => "verification unmatching (retries not limited)",
0x6381 => "file full due to last writing",
0x6400 => "file control information failure",
0x6581 => "writing to the memory failed",
0x6700 => "incorrect Lc/Le field",
0x6881 => "access with the specified logical channel number not provided",
0x6882 => "secure messaging feature not provided",
0x6981 => "command conflicting the file structure",
0x6982 => "security status not fulfilled",
0x6984 => "referenced IEF locked",
0x6985 => "command use condition not fulfilled",
0x6986 => "no current EF",
0x6987 => "no data object for secure messaging",
0x6988 => "secure messaging CCS illegal",
0x6A80 => "incorrect data field tag",
0x6A81 => "feature not provided",
0x6A82 => "no file to be accessed",
0x6A83 => "no record to be accessed",
0x6A84 => "insufficient memory space in the file",
0x6A85 => "Lc value conflicting the TLV structure",
0x6A86 => "incorrect P1-P2 value",
0x6A87 => "Lc value conflicting P1-P2",
0x6A88 => "referenced key not correctly set",
0x6B00 => "offset specified out of the EF range",
0x6D00 => "INS not provided",
0x6E00 => "class not provided",
0x6F00 => "self-diagnosis failure",
_ => return None,
})
}
}
impl fmt::Display for StatusWord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SW={:04X}", self.0)?;
if let Some(retries) = self.retries_remaining() {
return write!(f, " (verification failed, {retries} attempt(s) remaining)");
}
if let Some(description) = self.description() {
write!(f, " ({description})")?;
}
Ok(())
}
}
impl From<u16> for StatusWord {
fn from(value: u16) -> Self {
StatusWord(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_each_apdu_case() {
assert_eq!(
Command::new(0x00, 0xA4, 0x04, 0x0C).to_bytes().unwrap(),
[0x00, 0xA4, 0x04, 0x0C]
);
assert_eq!(
Command::with_le(0x00, 0xB0, 0x00, 0x00, 256)
.to_bytes()
.unwrap(),
[0x00, 0xB0, 0x00, 0x00, 0x00]
);
assert_eq!(
Command::with_data(0x00, 0xA4, 0x02, 0x0C, [0x00, 0x01])
.to_bytes()
.unwrap(),
[0x00, 0xA4, 0x02, 0x0C, 0x02, 0x00, 0x01]
);
assert_eq!(
Command::with_data_le(0x80, 0x2A, 0x00, 0x80, [0xAA], 256)
.to_bytes()
.unwrap(),
[0x80, 0x2A, 0x00, 0x80, 0x01, 0xAA, 0x00]
);
}
#[test]
fn switches_to_the_extended_encoding_past_255_bytes() {
let short = Command::with_data(0x80, 0xA2, 0x06, 0xC1, vec![0xAA; 255])
.to_bytes()
.unwrap();
assert_eq!(short[..5], [0x80, 0xA2, 0x06, 0xC1, 0xFF]);
assert_eq!(short.len(), 5 + 255);
let long = Command::with_data(0x80, 0xA2, 0x06, 0xC1, vec![0xAA; 307])
.to_bytes()
.unwrap();
assert_eq!(long[..7], [0x80, 0xA2, 0x06, 0xC1, 0x00, 0x01, 0x33]);
assert_eq!(long.len(), 7 + 307);
let both = Command::with_data_le(0x80, 0xA2, 0x00, 0xC1, vec![0xAA; 300], 65536)
.to_bytes()
.unwrap();
assert_eq!(both[..7], [0x80, 0xA2, 0x00, 0xC1, 0x00, 0x01, 0x2C]);
assert_eq!(both[both.len() - 2..], [0x00, 0x00]);
}
#[test]
fn rejects_data_no_encoding_can_carry() {
let cmd = Command::with_data(0x00, 0x20, 0x00, 0x80, vec![0u8; 0x10000]);
assert!(matches!(cmd.to_bytes(), Err(Error::DataTooLong(0x10000))));
}
#[test]
fn splits_response_into_data_and_status() {
let resp = Response::parse(&[0xDE, 0xAD, 0x90, 0x00]).unwrap();
assert_eq!(resp.data, [0xDE, 0xAD]);
assert_eq!(resp.status, StatusWord::SUCCESS);
let resp = Response::parse(&[0x90, 0x00]).unwrap();
assert!(resp.data.is_empty());
assert!(resp.status.is_success());
assert!(matches!(
Response::parse(&[0x90]),
Err(Error::ShortResponse(1))
));
}
#[test]
fn decodes_retry_counter() {
assert_eq!(StatusWord::new(0x63C3).retries_remaining(), Some(3));
assert_eq!(StatusWord::new(0x63C0).retries_remaining(), Some(0));
assert_eq!(StatusWord::new(0x6982).retries_remaining(), None);
}
#[test]
fn maps_status_to_pin_errors() {
assert!(matches!(
Error::from_status(StatusWord::new(0x63C2)),
Error::PinIncorrect { retries: Some(2) }
));
assert!(matches!(
Error::from_status(StatusWord::new(0x6300)),
Error::PinIncorrect { retries: None }
));
assert!(matches!(
Error::from_status(StatusWord::new(0x63C0)),
Error::PinBlocked
));
assert!(matches!(
Error::from_status(StatusWord::new(0x6984)),
Error::PinBlocked
));
assert!(matches!(
Error::from_status(StatusWord::new(0x6A82)),
Error::Status(_)
));
}
#[test]
fn separates_warnings_from_success_and_failure() {
assert!(StatusWord::new(0x6281).is_warning());
assert!(StatusWord::new(0x63C1).is_warning());
assert!(!StatusWord::SUCCESS.is_warning());
assert!(!StatusWord::new(0x6A82).is_warning());
}
#[test]
fn builds_cla_bytes() {
assert_eq!(cla::with_channel(cla::USER, 1), 0x01);
assert_eq!(cla::with_channel(cla::SYSTEM, 1), 0x81);
assert_eq!(
cla::with_channel(cla::SYSTEM | cla::SM_WITH_INTEGRITY, 0),
0x8C
);
}
}