use crate::errors::{CardError, CardOp, ErrCtx, PcscOp};
use anyhow::Result;
use std::ffi::CString;
pub(crate) struct PcscSession {
card: Option<pcsc::Card>,
secured: bool,
pub(super) mgmt_algo: Option<crate::piv::MgmtAlgo>,
pub(super) authenticated: Option<zeroize::Zeroizing<String>>,
}
impl PcscSession {
pub(crate) fn open(reader: &str) -> Result<Self> {
Self::open_with_mode(reader, pcsc::ShareMode::Shared)
}
pub(super) fn open_with_mode(reader: &str, mode: pcsc::ShareMode) -> Result<Self> {
let ctx = pcsc::Context::establish(pcsc::Scope::User)
.map_err(|e| CardError::pcsc(PcscOp::Establish, e))?;
let mut attempts = 0;
loop {
let card = connect_reader_mode(&ctx, reader, mode)?;
let mut session = Self {
card: Some(card),
secured: false,
mgmt_algo: None,
authenticated: None,
};
match session.select_piv() {
Ok(()) => return Ok(session),
Err(e) if attempts < RESET_RETRIES && is_reset(&e) => attempts += 1,
Err(e) => return Err(e),
}
}
}
pub(crate) fn select_piv(&mut self) -> Result<()> {
self.transmit_check(SELECT_PIV, CardOp::SelectPiv)?;
Ok(())
}
pub(crate) fn card(&self) -> &pcsc::Card {
self.card.as_ref().expect("an open connection")
}
pub(crate) fn transmit_raw(&mut self, apdu: &[u8]) -> Result<Vec<u8>> {
if !apdu.get(1).is_some_and(|ins| READ_ONLY_INS.contains(ins)) {
self.secured = true;
}
transmit_raw_card(self.card(), apdu)
}
pub(crate) fn err_ctx(&self) -> ErrCtx {
ErrCtx {
algo: self.mgmt_algo,
}
}
pub(crate) fn status_error(&self, op: CardOp, sw1: u8, sw2: u8) -> CardError {
CardError::status(op, sw1, sw2).with_ctx(self.err_ctx())
}
pub(crate) fn transmit_check(&mut self, apdu: &[u8], op: CardOp) -> Result<Vec<u8>> {
let mut resp = self.transmit_raw(apdu)?;
let mut data = Vec::new();
loop {
let n = resp.len();
if n < 2 {
return Err(
CardError::protocol(op, format!("response too short ({n} bytes)")).into(),
);
}
let sw1 = resp[n - 2];
let sw2 = resp[n - 1];
data.extend_from_slice(&resp[..n - 2]);
if sw1 == 0x90 && sw2 == 0x00 {
break; }
if sw1 == 0x61 {
let le = if sw2 == 0x00 { 0x00u8 } else { sw2 };
let get_resp = [0x00, 0xC0, 0x00, 0x00, le];
resp = self.transmit_raw(&get_resp)?;
continue;
}
return Err(self.status_error(op, sw1, sw2).into());
}
Ok(data)
}
}
impl Drop for PcscSession {
fn drop(&mut self) {
if let Some(card) = self.card.take() {
let disposition = if self.secured {
pcsc::Disposition::ResetCard
} else {
pcsc::Disposition::LeaveCard
};
let _ = card.disconnect(disposition);
}
}
}
const READ_ONLY_INS: [u8; 6] = [0xA4, 0xCB, 0xC0, 0xF7, 0xF8, 0xFD];
pub(crate) const RESET_RETRIES: u32 = 10;
pub(crate) fn is_reset(e: &anyhow::Error) -> bool {
matches!(
e.downcast_ref::<CardError>(),
Some(CardError::Pcsc {
code: crate::errors::PcscCode::ResetCard,
..
})
) || matches!(
e.downcast_ref::<CardError>(),
Some(CardError::Pcsc { code: crate::errors::PcscCode::Other(c), .. }) if c == "ProtoMismatch"
)
}
const RESET_PATIENCE: std::time::Duration = std::time::Duration::from_secs(30);
pub(crate) const SELECT_PIV: &[u8] = &[0x00, 0xA4, 0x04, 0x00, 0x05, 0xA0, 0x00, 0x00, 0x03, 0x08];
pub(crate) fn transmit_raw_card(card: &pcsc::Card, apdu: &[u8]) -> Result<Vec<u8>> {
let mut buf = vec![0u8; pcsc::MAX_BUFFER_SIZE_EXTENDED];
let resp = card
.transmit(apdu, &mut buf)
.map_err(|e| CardError::pcsc(PcscOp::Transmit, e))?;
Ok(resp.to_vec())
}
pub(crate) fn connect_reader_mode(
ctx: &pcsc::Context,
reader: &str,
mode: pcsc::ShareMode,
) -> Result<pcsc::Card> {
let cstring = CString::new(reader).map_err(|_| anyhow::anyhow!("invalid reader name"))?;
crate::piv::lock::waiting(|| {
let mut card = ctx
.connect(&cstring, mode, pcsc::Protocols::ANY)
.map_err(|e| CardError::pcsc(PcscOp::Connect, e))?;
let start = std::time::Instant::now();
loop {
match card.transaction().map(std::mem::forget) {
Ok(()) => return Ok(card),
Err(pcsc::Error::ResetCard) if start.elapsed() < RESET_PATIENCE => {
card.reconnect(mode, pcsc::Protocols::ANY, pcsc::Disposition::LeaveCard)
.map_err(|e| CardError::pcsc(PcscOp::Connect, e))?;
}
Err(e) => return Err(CardError::pcsc(PcscOp::Lock, e).into()),
}
}
})
}