use crate::{
apdu::Response,
apdu::{Apdu, Ins, StatusWords},
consts::{CB_BUF_MAX, CB_OBJ_MAX},
error::{Error, Result},
otp,
piv::{self, AlgorithmId, SlotId},
serialization::*,
yubikey::*,
Buffer, ObjectId,
};
use log::{error, trace};
use zeroize::Zeroizing;
#[cfg(feature = "untested")]
use crate::mgm::{MgmKey, DES_LEN_3DES};
const CB_PIN_MAX: usize = 8;
#[cfg(feature = "untested")]
pub(crate) enum ChangeRefAction {
ChangePin,
ChangePuk,
UnblockPin,
}
pub(crate) struct Transaction<'tx> {
inner: pcsc::Transaction<'tx>,
}
impl<'tx> Transaction<'tx> {
pub fn new(card: &'tx mut pcsc::Card) -> Result<Self> {
Ok(Transaction {
inner: card.transaction()?,
})
}
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Vec<u8>> {
trace!(">>> {:?}", send_buffer);
let mut recv_buffer = vec![0u8; recv_len];
let len = self
.inner
.transmit(send_buffer, recv_buffer.as_mut())?
.len();
recv_buffer.truncate(len);
Ok(recv_buffer)
}
pub fn select_application(&self) -> Result<()> {
let response = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(piv::APPLET_ID)
.transmit(self, 0xFF)
.map_err(|e| {
error!("failed communicating with card: '{}'", e);
e
})?;
if !response.is_success() {
error!(
"failed selecting application: {:04x}",
response.status_words().code()
);
return Err(match response.status_words() {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: piv::APPLET_NAME,
},
_ => Error::GenericError,
});
}
Ok(())
}
pub fn get_version(&self) -> Result<Version> {
let response = Apdu::new(Ins::GetVersion).transmit(self, 261)?;
if !response.is_success() {
return Err(Error::GenericError);
}
Ok(response.data()[..3].try_into().map(Version::new)?)
}
pub fn get_serial(&self, version: Version) -> Result<Serial> {
match version.major {
4 => {
let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(otp::APPLET_ID)
.transmit(self, 0xFF)?
.status_words();
if !sw.is_success() {
error!("failed selecting yk application: {:04x}", sw.code());
return Err(match sw {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: otp::APPLET_NAME,
},
_ => Error::GenericError,
});
}
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
if !response.is_success() {
error!(
"failed retrieving serial number: {:04x}",
response.status_words().code()
);
return Err(Error::GenericError);
}
let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(piv::APPLET_ID)
.transmit(self, 0xFF)?
.status_words();
if !sw.is_success() {
error!("failed selecting application: {:04x}", sw.code());
return Err(match sw {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: piv::APPLET_NAME,
},
_ => Error::GenericError,
});
}
response.data().try_into()
}
5 => {
let response = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?;
if !response.is_success() {
error!(
"failed retrieving serial number: {:04x}",
response.status_words().code()
);
return Err(Error::GenericError);
}
response.data().try_into()
}
_ => Err(Error::NotSupported),
}
}
pub fn verify_pin(&self, pin: &[u8]) -> Result<()> {
if pin.len() > CB_PIN_MAX {
return Err(Error::SizeError);
}
let mut query = Apdu::new(Ins::Verify);
query.params(0x00, 0x80);
if !pin.is_empty() {
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
data[0..pin.len()].copy_from_slice(pin);
query.data(data.as_ref());
}
let response = query.transmit(self, 261)?;
match response.status_words() {
StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }),
StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
_ => Err(Error::GenericError),
}
}
#[cfg(feature = "untested")]
pub fn change_ref(
&self,
action: ChangeRefAction,
current_pin: &[u8],
new_pin: &[u8],
) -> Result<()> {
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
return Err(Error::SizeError);
}
const PIN: u8 = 0x80;
const PUK: u8 = 0x81;
let templ = match action {
ChangeRefAction::ChangePin => [0, Ins::ChangeReference.code(), 0, PIN],
ChangeRefAction::ChangePuk => [0, Ins::ChangeReference.code(), 0, PUK],
ChangeRefAction::UnblockPin => [0, Ins::ResetRetry.code(), 0, PIN],
};
let mut indata = Zeroizing::new([0xff; CB_PIN_MAX * 2]);
indata[0..current_pin.len()].copy_from_slice(current_pin);
indata[CB_PIN_MAX..CB_PIN_MAX + new_pin.len()].copy_from_slice(new_pin);
let status_words = self
.transfer_data(&templ, indata.as_ref(), 0xFF)?
.status_words();
match status_words {
StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::PinLocked),
StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
_ => {
error!(
"failed changing pin, token response code: {:x}.",
status_words.code()
);
Err(Error::GenericError)
}
}
}
#[cfg(feature = "untested")]
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
let p2 = if require_touch { 0xfe } else { 0xff };
let mut data = [0u8; DES_LEN_3DES + 3];
data[0] = ALGO_3DES;
data[1] = KEY_CARDMGM;
data[2] = DES_LEN_3DES as u8;
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
let status_words = Apdu::new(Ins::SetMgmKey)
.params(0xff, p2)
.data(data)
.transmit(self, 261)?
.status_words();
if !status_words.is_success() {
return Err(Error::GenericError);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn authenticated_command(
&self,
sign_in: &[u8],
algorithm: AlgorithmId,
key: SlotId,
decipher: bool,
) -> Result<Buffer> {
let in_len = sign_in.len();
let mut indata = [0u8; 1024];
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
let key_len = if let AlgorithmId::Rsa1024 = algorithm {
128
} else {
256
};
if in_len != key_len {
return Err(Error::SizeError);
}
}
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let key_len = if let AlgorithmId::EccP256 = algorithm {
32
} else {
48
};
if (!decipher && (in_len > key_len)) || (decipher && (in_len != (key_len * 2) + 1))
{
return Err(Error::SizeError);
}
}
}
let bytes = if in_len < 0x80 {
1
} else if in_len < 0xff {
2
} else {
3
};
let offset = Tlv::write_as(&mut indata, 0x7c, in_len + bytes + 3, |buf| {
assert_eq!(Tlv::write(buf, 0x82, &[]).expect("large enough"), 2);
assert_eq!(
Tlv::write(
&mut buf[2..],
match (algorithm, decipher) {
(AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85,
_ => 0x81,
},
sign_in
)
.expect("large enough"),
1 + bytes + in_len
);
})?;
let response = self
.transfer_data(&templ, &indata[..offset], 1024)
.map_err(|e| {
error!("sign command failed to communicate: {}", e);
e
})?;
if !response.is_success() {
error!("failed sign command with code {:x}", response.code());
if response.status_words() == StatusWords::SecurityStatusError {
return Err(Error::AuthenticationError);
} else {
return Err(Error::GenericError);
}
}
let (_, outer_tlv) = Tlv::parse(response.data())?;
if outer_tlv.tag != 0x7c {
error!("failed parsing signature reply (0x7c byte)");
return Err(Error::ParseError);
}
let (_, inner_tlv) = Tlv::parse(outer_tlv.value)?;
if inner_tlv.tag != 0x82 {
error!("failed parsing signature reply (0x82 byte)");
return Err(Error::ParseError);
}
Ok(Buffer::new(inner_tlv.value.into()))
}
pub fn transfer_data(&self, templ: &[u8], in_data: &[u8], max_out: usize) -> Result<Response> {
let mut in_offset = 0;
let mut out_data = vec![];
let mut sw;
loop {
let mut this_size = 0xff;
let cla = if in_offset + 0xff < in_data.len() {
0x10
} else {
this_size = in_data.len() - in_offset;
templ[0]
};
trace!("going to send {} bytes in this go", this_size);
let response = Apdu::new(templ[1])
.cla(cla)
.params(templ[2], templ[3])
.data(&in_data[in_offset..(in_offset + this_size)])
.transmit(self, 261)?;
sw = response.status_words();
match sw {
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
_ => return Ok(Response::new(sw, out_data)),
}
if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
error!(
"output buffer too small: wanted to write {}, max was {}",
out_data.len() - response.data().len(),
max_out
);
return Err(Error::SizeError);
}
out_data.extend_from_slice(&response.data()[..response.data().len()]);
in_offset += this_size;
if in_offset >= in_data.len() {
break;
}
}
while let StatusWords::BytesRemaining { len } = sw {
trace!("The card indicates there is {} bytes more data for us", len);
let response = Apdu::new(Ins::GetResponseApdu).transmit(self, 261)?;
sw = response.status_words();
match sw {
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
_ => return Ok(Response::new(sw, vec![])),
}
if out_data.len() + response.data().len() > max_out {
error!(
"output buffer too small: wanted to write {}, max was {}",
out_data.len() + response.data().len(),
max_out
);
return Err(Error::SizeError);
}
out_data.extend_from_slice(&response.data()[..response.data().len()]);
}
Ok(Response::new(sw, out_data))
}
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer> {
let mut indata = [0u8; 5];
let templ = [0, Ins::GetData.code(), 0x3f, 0xff];
let mut inlen = indata.len();
let indata_remaining = set_object(object_id, &mut indata);
inlen -= indata_remaining.len();
let response = self.transfer_data(&templ, &indata[..inlen], CB_BUF_MAX)?;
if !response.is_success() {
if response.status_words() == StatusWords::NotFoundError {
return Err(Error::NotFound);
} else {
return Err(Error::GenericError);
}
}
let (remaining, tlv) = Tlv::parse(response.data())?;
if !remaining.is_empty() {
error!(
"invalid length indicated in object: total len is {} but indicated length is {}",
tlv.value.len() + remaining.len(),
tlv.value.len()
);
return Err(Error::SizeError);
}
Ok(Zeroizing::new(tlv.value.to_vec()))
}
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<()> {
let templ = [0, Ins::PutData.code(), 0x3f, 0xff];
let mut data = [0u8; CB_BUF_MAX];
if indata.len() > CB_OBJ_MAX {
return Err(Error::SizeError);
}
let mut len = data.len();
let mut data_remaining = set_object(object_id, &mut data);
let offset = Tlv::write(data_remaining, 0x53, indata)?;
data_remaining = &mut data_remaining[offset..];
len -= data_remaining.len();
let status_words = self
.transfer_data(&templ, &data[..len], 255)?
.status_words();
match status_words {
StatusWords::Success => Ok(()),
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
_ => Err(Error::GenericError),
}
}
}