use crate::{
apdu::{Apdu, Ins},
cccid::CccId,
chuid::ChuId,
config::Config,
error::{Error, Result},
mgm::MgmKey,
piv,
reader::{Context, Reader},
transaction::Transaction,
};
use log::{error, info};
use pcsc::{Card, Disposition};
use rand_core::{OsRng, RngCore};
use std::{
fmt::{self, Display},
str::FromStr,
};
#[cfg(feature = "untested")]
use {
crate::{
apdu::StatusWords,
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP},
metadata::AdminData,
mgm,
transaction::ChangeRefAction,
Buffer, ObjectId,
},
secrecy::ExposeSecret,
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 TryFrom<&[u8]> for Serial {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self> {
if bytes.len() > 4 {
return Err(Error::SizeError);
}
let mut arr = [0u8; 4];
arr[(4 - bytes.len())..].copy_from_slice(bytes);
Ok(Self(u32::from_be_bytes(arr)))
}
}
impl FromStr for Serial {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
s.parse().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 fmt::Debug for YubiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("YubiKey")
.field("name", &self.name)
.field("version", &self.version)
.field("serial", &self.serial)
.finish_non_exhaustive()
}
}
impl YubiKey {
pub fn open() -> Result<Self> {
let mut yubikey: Option<Self> = None;
let mut readers = Context::open()?;
for reader in readers.iter()? {
if let Ok(yk_found) = reader.open() {
if let Some(yk_stored) = yubikey {
let _ = yk_stored.disconnect(pcsc::Disposition::LeaveCard);
let _ = yk_found.disconnect(pcsc::Disposition::LeaveCard);
error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
} else {
yubikey = Some(yk_found);
}
}
}
if let Some(yubikey) = yubikey {
Ok(yubikey)
} else {
error!("no YubiKey detected!");
Err(Error::NotFound)
}
}
pub fn open_by_serial(serial: Serial) -> Result<Self> {
let mut readers = Context::open()?;
let mut open_error = None;
for reader in readers.iter()? {
let yubikey = match reader.open() {
Ok(yk) => yk,
Err(e) => {
if open_error.is_none() {
if let Error::PcscError {
inner: Some(pcsc::Error::SharingViolation),
} = e
{
open_error = Some(e);
}
}
continue;
}
};
if serial == yubikey.serial() {
return Ok(yubikey);
} else {
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
}
}
Err(if let Some(e) = open_error {
e
} else {
error!("no YubiKey detected with serial: {}", serial);
Error::NotFound
})
}
#[cfg(feature = "untested")]
pub fn reconnect(&mut self) -> Result<()> {
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 fn disconnect(self, disposition: Disposition) -> core::result::Result<(), (Self, Error)> {
let Self {
card,
name,
pin,
version,
serial,
} = self;
card.disconnect(disposition).map_err(|(card, e)| {
(
Self {
card,
name,
pin,
version,
serial,
},
e.into(),
)
})
}
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> {
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> {
Config::get(self)
}
pub fn chuid(&mut self) -> Result<ChuId> {
ChuId::get(self)
}
pub fn cccid(&mut self) -> Result<CccId> {
CccId::get(self)
}
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<()> {
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()?);
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;
OsRng.fill_bytes(&mut data[14..22]);
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(())
}
pub fn piv_keys(&mut self) -> Result<Vec<piv::Key>> {
piv::Key::list(self)
}
#[cfg(feature = "untested")]
pub fn deauthenticate(&mut self) -> Result<()> {
let txn = self.begin_transaction()?;
let status_words = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(mgm::APPLET_ID)
.transmit(&txn, 255)?
.status_words();
if !status_words.is_success() {
error!(
"Failed selecting mgmt application: {:04x}",
status_words.code()
);
return Err(match status_words {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: mgm::APPLET_NAME,
},
_ => Error::GenericError,
});
}
Ok(())
}
pub fn verify_pin(&mut self, pin: &[u8]) -> Result<()> {
{
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> {
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<()> {
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<()> {
{
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<()> {
let txn = yubikey.begin_transaction()?;
let mut admin_data = AdminData::read(&txn)?;
let tnow = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs()
.to_le_bytes();
admin_data
.set_item(TAG_ADMIN_TIMESTAMP, &tnow)
.map_err(|e| {
error!("could not set pin timestamp, err = {}", e);
e
})?;
admin_data.write(&txn).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<()> {
let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
}
#[cfg(feature = "untested")]
pub fn block_puk(&mut self) -> Result<()> {
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
let mut tries_remaining: i32 = -1;
let mut flags = [0];
let txn = self.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;
}
}
}
let mut admin_data = AdminData::read(&txn)
.map(|data| {
if let Ok(item) = data.get_item(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()
);
}
}
data
})
.unwrap_or_default();
flags[0] |= ADMIN_FLAGS_1_PUK_BLOCKED;
if admin_data.set_item(TAG_ADMIN_FLAGS_1, &flags).is_ok() {
if admin_data.write(&txn).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<()> {
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> {
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<()> {
let txn = self.begin_transaction()?;
txn.save_object(object_id, indata)
}
#[cfg(feature = "untested")]
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> {
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()
.get(4..12)
.ok_or(Error::SizeError)?
.try_into()?)
}
#[cfg(feature = "untested")]
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> {
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<()> {
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> {
let mut card = reader.connect().map_err(|e| {
error!("error connecting to reader '{}': {}", reader.name(), e);
e
})?;
info!("connected to reader: {}", reader.name());
let mut app_version_serial = || -> Result<(Version, Serial)> {
let txn = Transaction::new(&mut card)?;
txn.select_application()?;
let v = txn.get_version()?;
let s = txn.get_serial(v)?;
Ok((v, s))
};
match app_version_serial() {
Err(e) => {
error!("Could not use reader: {}", e);
if let Err((_, e)) = card.disconnect(pcsc::Disposition::LeaveCard) {
error!("Failed to disconnect gracefully from card: {}", e);
}
Err(e)
}
Ok((version, serial)) => {
let yubikey = YubiKey {
card,
name: String::from(reader.name()),
pin: None,
version,
serial,
};
Ok(yubikey)
}
}
}
}