use crate::{
apdu::{Ins, APDU},
cccid::CCC,
chuid::CHUID,
config::Config,
error::Error,
mgm::MgmKey,
readers::{Reader, Readers},
transaction::Transaction,
};
use log::{error, info};
use pcsc::Card;
use std::{
convert::{TryFrom, TryInto},
fmt::{self, Display},
str::FromStr,
};
#[cfg(feature = "untested")]
use crate::{
apdu::StatusWords, metadata, transaction::ChangeRefAction, Buffer, ObjectId, CB_BUF_MAX,
CB_OBJ_MAX, MGMT_AID, TAG_ADMIN, TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP,
};
use getrandom::getrandom;
#[cfg(feature = "untested")]
use secrecy::ExposeSecret;
#[cfg(feature = "untested")]
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
pub(crate) const ALGO_3DES: u8 = 0x03;
pub(crate) const KEY_CARDMGM: u8 = 0x9b;
const TAG_DYN_AUTH: u8 = 0x7c;
pub type CachedPin = secrecy::SecretVec<u8>;
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct Serial(pub u32);
impl From<u32> for Serial {
fn from(num: u32) -> Serial {
Serial(num)
}
}
impl From<Serial> for u32 {
fn from(serial: Serial) -> u32 {
serial.0
}
}
impl FromStr for Serial {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
u32::from_str(s).map(Serial).map_err(|_| Error::ParseError)
}
}
impl Display for Serial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Version {
pub major: u8,
pub minor: u8,
pub patch: u8,
}
impl Version {
pub fn new(bytes: [u8; 3]) -> Version {
Version {
major: bytes[0],
minor: bytes[1],
patch: bytes[2],
}
}
}
impl Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[cfg_attr(not(feature = "untested"), allow(dead_code))]
pub struct YubiKey {
pub(crate) card: Card,
pub(crate) name: String,
pub(crate) pin: Option<CachedPin>,
pub(crate) version: Version,
pub(crate) serial: Serial,
}
impl YubiKey {
pub fn open() -> Result<Self, Error> {
let mut readers = Readers::open().map_err(|e| match e {
Error::PcscError {
inner: Some(pcsc::Error::NoReadersAvailable),
} => Error::NotFound,
other => other,
})?;
let mut reader_iter = readers.iter()?;
if let Some(reader) = reader_iter.next() {
if reader_iter.next().is_some() {
error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
}
return reader.open();
}
error!("no YubiKey detected!");
Err(Error::NotFound)
}
pub fn open_by_serial(serial: Serial) -> Result<Self, Error> {
let mut readers = Readers::open().map_err(|e| match e {
Error::PcscError {
inner: Some(pcsc::Error::NoReadersAvailable),
} => Error::NotFound,
other => other,
})?;
for reader in readers.iter()? {
let yubikey = match reader.open() {
Ok(yk) => yk,
Err(_) => continue,
};
if serial == yubikey.serial() {
return Ok(yubikey);
}
}
error!("no YubiKey detected with serial: {}", serial);
Err(Error::NotFound)
}
#[cfg(feature = "untested")]
pub fn reconnect(&mut self) -> Result<(), Error> {
info!("trying to reconnect to current reader");
self.card.reconnect(
pcsc::ShareMode::Shared,
pcsc::Protocols::T1,
pcsc::Disposition::ResetCard,
)?;
let pin = self
.pin
.as_ref()
.map(|p| Buffer::new(p.expose_secret().clone()));
let txn = Transaction::new(&mut self.card)?;
txn.select_application()?;
if let Some(p) = &pin {
txn.verify_pin(p)?;
}
Ok(())
}
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>, Error> {
Ok(Transaction::new(&mut self.card)?)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> Version {
self.version
}
pub fn serial(&self) -> Serial {
self.serial
}
pub fn config(&mut self) -> Result<Config, Error> {
Config::get(self)
}
pub fn chuid(&mut self) -> Result<CHUID, Error> {
CHUID::get(self)
}
pub fn cccid(&mut self) -> Result<CCC, Error> {
CCC::get(self)
}
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<(), Error> {
let txn = self.begin_transaction()?;
let challenge = APDU::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
.data(&[TAG_DYN_AUTH, 0x02, 0x80, 0x00])
.transmit(&txn, 261)?;
if !challenge.is_success() || challenge.data().len() < 12 {
return Err(Error::AuthenticationError);
}
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap());
let mut data = [0u8; 22];
data[0] = TAG_DYN_AUTH;
data[1] = 20;
data[2] = 0x80;
data[3] = 8;
data[4..12].copy_from_slice(&response);
data[12] = 0x81;
data[13] = 8;
if getrandom(&mut data[14..22]).is_err() {
error!("failed getting randomness for authentication");
return Err(Error::RandomnessError);
}
let mut challenge = [0u8; 8];
challenge.copy_from_slice(&data[14..22]);
let authentication = APDU::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
.data(&data)
.transmit(&txn, 261)?;
if !authentication.is_success() {
return Err(Error::AuthenticationError);
}
let response = mgm_key.encrypt(&challenge);
use subtle::ConstantTimeEq;
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
return Err(Error::AuthenticationError);
}
Ok(())
}
#[cfg(feature = "untested")]
pub fn deauthenticate(&mut self) -> Result<(), Error> {
let txn = self.begin_transaction()?;
let status_words = APDU::new(Ins::SelectApplication)
.p1(0x04)
.data(MGMT_AID)
.transmit(&txn, 255)?
.status_words();
if !status_words.is_success() {
error!(
"Failed selecting mgmt application: {:04x}",
status_words.code()
);
return Err(Error::GenericError);
}
Ok(())
}
pub fn verify_pin(&mut self, pin: &[u8]) -> Result<(), Error> {
{
let txn = self.begin_transaction()?;
txn.verify_pin(pin)?;
}
if !pin.is_empty() {
self.pin = Some(CachedPin::new(pin.into()))
}
Ok(())
}
pub fn get_pin_retries(&mut self) -> Result<u8, Error> {
let txn = self.begin_transaction()?;
txn.select_application()?;
match txn.verify_pin(&[]) {
Ok(()) => Ok(0),
Err(Error::WrongPin { tries }) => Ok(tries),
Err(e) => Err(e),
}
}
#[cfg(feature = "untested")]
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<(), Error> {
if pin_tries == 0 || puk_tries == 0 {
return Ok(());
}
let txn = self.begin_transaction()?;
let templ = [0, Ins::SetPinRetries.code(), pin_tries, puk_tries];
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
match status_words {
StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::AuthenticationError),
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
_ => Err(Error::GenericError),
}
}
#[cfg(feature = "untested")]
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
{
let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::ChangePin, current_pin, new_pin)?;
}
if !new_pin.is_empty() {
self.pin = Some(CachedPin::new(new_pin.into()));
}
Ok(())
}
#[cfg(feature = "untested")]
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<(), Error> {
let mut data = [0u8; CB_BUF_MAX];
let txn = yubikey.begin_transaction()?;
let buffer = metadata::read(&txn, TAG_ADMIN)?;
let mut cb_data = buffer.len();
data[..cb_data].copy_from_slice(&buffer);
let tnow = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_le_bytes();
metadata::set_item(
&mut data,
&mut cb_data,
CB_OBJ_MAX,
TAG_ADMIN_TIMESTAMP,
&tnow,
)
.map_err(|e| {
error!("could not set pin timestamp, err = {}", e);
e
})?;
metadata::write(&txn, TAG_ADMIN, &data).map_err(|e| {
error!("could not write admin data, err = {}", e);
e
})?;
Ok(())
}
#[cfg(feature = "untested")]
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<(), Error> {
let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
}
#[cfg(feature = "untested")]
pub fn block_puk(yubikey: &mut YubiKey) -> Result<(), Error> {
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
let mut tries_remaining: i32 = -1;
let mut flags = [0];
let txn = yubikey.begin_transaction()?;
while tries_remaining != 0 {
let res = txn.change_ref(ChangeRefAction::ChangePuk, &puk, &puk);
match res {
Ok(()) => puk[0] += 1,
Err(Error::WrongPin { tries }) => {
tries_remaining = tries as i32;
continue;
}
Err(e) => {
if e != Error::PinLocked {
continue;
}
tries_remaining = 0;
}
}
}
if let Ok(data) = metadata::read(&txn, TAG_ADMIN) {
if let Ok(item) = metadata::get_item(&data, TAG_ADMIN_FLAGS_1) {
if item.len() == flags.len() {
flags.copy_from_slice(item)
} else {
error!(
"admin flags exist, but are incorrect size: {} (expected {})",
item.len(),
flags.len()
);
}
}
}
flags[0] |= ADMIN_FLAGS_1_PUK_BLOCKED;
let mut data = [0u8; CB_BUF_MAX];
let mut cb_data: usize = data.len();
if metadata::set_item(
&mut data,
&mut cb_data,
CB_OBJ_MAX,
TAG_ADMIN_FLAGS_1,
&flags,
)
.is_ok()
{
if metadata::write(&txn, TAG_ADMIN, &data[..cb_data]).is_err() {
error!("could not write admin metadata");
}
} else {
error!("could not set admin flags");
}
Ok(())
}
#[cfg(feature = "untested")]
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), Error> {
let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin)
}
#[cfg(feature = "untested")]
pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer, Error> {
let txn = self.begin_transaction()?;
txn.fetch_object(object_id)
}
#[cfg(feature = "untested")]
pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<(), Error> {
let txn = self.begin_transaction()?;
txn.save_object(object_id, indata)
}
#[cfg(feature = "untested")]
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
let txn = self.begin_transaction()?;
let response = APDU::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
.data(&[0x7c, 0x02, 0x81, 0x00])
.transmit(&txn, 261)?;
if !response.is_success() {
return Err(Error::AuthenticationError);
}
Ok(response.data()[4..12].try_into().unwrap())
}
#[cfg(feature = "untested")]
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<(), Error> {
let mut data = [0u8; 12];
data[0] = 0x7c;
data[1] = 0x0a;
data[2] = 0x82;
data[3] = 0x08;
data[4..12].copy_from_slice(&response);
let txn = self.begin_transaction()?;
let status_words = APDU::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
.data(&data)
.transmit(&txn, 261)?
.status_words();
if !status_words.is_success() {
return Err(Error::AuthenticationError);
}
Ok(())
}
#[cfg(feature = "untested")]
pub fn reset_device(&mut self) -> Result<(), Error> {
let templ = [0, Ins::Reset.code(), 0, 0];
let txn = self.begin_transaction()?;
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
if !status_words.is_success() {
return Err(Error::GenericError);
}
Ok(())
}
}
impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error;
fn try_from(reader: &'a Reader<'_>) -> Result<Self, Error> {
let mut card = reader.connect().map_err(|e| {
error!("error connecting to reader '{}': {}", reader.name(), e);
e
})?;
info!("connected to reader: {}", reader.name());
let (version, serial) = {
let txn = Transaction::new(&mut card)?;
txn.select_application()?;
let v = txn.get_version()?;
let s = txn.get_serial(v)?;
(v, s)
};
let yubikey = YubiKey {
card,
name: String::from(reader.name()),
pin: None,
version,
serial,
};
Ok(yubikey)
}
}