use crate::crypto;
use crate::ctap2::client_data::{ClientDataHash, CollectedClientData};
use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinAuth, PinError};
use crate::errors::AuthenticatorError;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::FidoDevice;
use serde_cbor::{error::Error as CborError, Value};
use serde_json as json;
use std::error::Error as StdErrorT;
use std::fmt;
use std::io::{Read, Write};
pub(crate) mod client_pin;
pub(crate) mod get_assertion;
pub(crate) mod get_info;
pub(crate) mod get_next_assertion;
pub(crate) mod get_version;
pub(crate) mod make_credentials;
pub(crate) mod reset;
pub(crate) mod selection;
pub trait Request<T>
where
Self: fmt::Debug,
Self: RequestCtap1<Output = T>,
Self: RequestCtap2<Output = T>,
{
fn is_ctap2_request(&self) -> bool;
}
#[derive(Debug)]
pub enum Retryable<T> {
Retry,
Error(T),
}
impl<T> Retryable<T> {
pub fn is_retry(&self) -> bool {
matches!(*self, Retryable::Retry)
}
pub fn is_error(&self) -> bool {
!self.is_retry()
}
}
impl<T> From<T> for Retryable<T> {
fn from(e: T) -> Self {
Retryable::Error(e)
}
}
pub trait RequestCtap1: fmt::Debug {
type Output;
fn apdu_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
where
Dev: FidoDevice + Read + Write + fmt::Debug;
fn handle_response_ctap1(
&self,
status: Result<(), ApduErrorStatus>,
input: &[u8],
) -> Result<Self::Output, Retryable<HIDError>>;
}
pub trait RequestCtap2: fmt::Debug {
type Output;
fn command() -> Command;
fn wire_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
where
Dev: FidoDevice + Read + Write + fmt::Debug;
fn handle_response_ctap2<Dev>(
&self,
dev: &mut Dev,
input: &[u8],
) -> Result<Self::Output, HIDError>
where
Dev: FidoDevice + Read + Write + fmt::Debug;
}
pub(crate) trait PinAuthCommand {
fn pin(&self) -> &Option<Pin>;
fn set_pin(&mut self, pin: Option<Pin>);
fn pin_auth(&self) -> &Option<PinAuth>;
fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>);
fn client_data(&self) -> &CollectedClientData;
fn unset_uv_option(&mut self);
fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> {
if !dev.supports_ctap2() {
self.set_pin_auth(None);
return Ok(());
}
let client_data_hash = self
.client_data()
.hash()
.map_err(|e| AuthenticatorError::HIDError(HIDError::Command(CommandError::Json(e))))?;
let pin_auth = match calculate_pin_auth(dev, &client_data_hash, &self.pin()) {
Ok(pin_auth) => pin_auth,
Err(e) => {
return Err(repackage_pin_errors(dev, e));
}
};
self.set_pin_auth(pin_auth);
Ok(())
}
}
pub(crate) fn repackage_pin_errors<D: FidoDevice>(
dev: &mut D,
error: AuthenticatorError,
) -> AuthenticatorError {
match error {
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinInvalid,
_,
))) => {
let cmd = GetRetries::new();
let retries = dev.send_cbor(&cmd).ok(); return AuthenticatorError::PinError(PinError::InvalidPin(retries));
}
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinAuthBlocked,
_,
))) => {
return AuthenticatorError::PinError(PinError::PinAuthBlocked);
}
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinBlocked,
_,
))) => {
return AuthenticatorError::PinError(PinError::PinBlocked);
}
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
StatusCode::PinRequired,
_,
))) => {
return AuthenticatorError::PinError(PinError::PinRequired);
}
err => {
return err;
}
}
}
#[repr(u8)]
#[derive(Debug)]
pub enum Command {
MakeCredentials = 0x01,
GetAssertion = 0x02,
GetInfo = 0x04,
ClientPin = 0x06,
Reset = 0x07,
GetNextAssertion = 0x08,
Selection = 0x0B,
}
impl Command {
#[cfg(test)]
pub fn from_u8(v: u8) -> Option<Command> {
match v {
0x01 => Some(Command::MakeCredentials),
0x02 => Some(Command::GetAssertion),
0x04 => Some(Command::GetInfo),
0x06 => Some(Command::ClientPin),
0x07 => Some(Command::Reset),
0x08 => Some(Command::GetNextAssertion),
_ => None,
}
}
}
#[derive(Debug)]
pub enum StatusCode {
OK,
InvalidCommand,
InvalidParameter,
InvalidLength,
InvalidSeq,
Timeout,
ChannelBusy,
LockRequired,
InvalidChannel,
CBORUnexpectedType,
InvalidCBOR,
MissingParameter,
LimitExceeded,
UnsupportedExtension,
CredentialExcluded,
Processing,
InvalidCredential,
UserActionPending,
OperationPending,
NoOperations,
UnsupportedAlgorithm,
OperationDenied,
KeyStoreFull,
NoOperationPending,
UnsupportedOption,
InvalidOption,
KeepaliveCancel,
NoCredentials,
UserActionTimeout,
NotAllowed,
PinInvalid,
PinBlocked,
PinAuthInvalid,
PinAuthBlocked,
PinNotSet,
PinRequired,
PinPolicyViolation,
PinTokenExpired,
RequestTooLarge,
ActionTimeout,
UpRequired,
Unknown(u8),
}
impl StatusCode {
fn is_ok(&self) -> bool {
matches!(*self, StatusCode::OK)
}
fn device_busy(&self) -> bool {
matches!(*self, StatusCode::ChannelBusy)
}
}
impl From<u8> for StatusCode {
fn from(value: u8) -> StatusCode {
match value {
0x00 => StatusCode::OK,
0x01 => StatusCode::InvalidCommand,
0x02 => StatusCode::InvalidParameter,
0x03 => StatusCode::InvalidLength,
0x04 => StatusCode::InvalidSeq,
0x05 => StatusCode::Timeout,
0x06 => StatusCode::ChannelBusy,
0x0A => StatusCode::LockRequired,
0x0B => StatusCode::InvalidChannel,
0x11 => StatusCode::CBORUnexpectedType,
0x12 => StatusCode::InvalidCBOR,
0x14 => StatusCode::MissingParameter,
0x15 => StatusCode::LimitExceeded,
0x16 => StatusCode::UnsupportedExtension,
0x19 => StatusCode::CredentialExcluded,
0x21 => StatusCode::Processing,
0x22 => StatusCode::InvalidCredential,
0x23 => StatusCode::UserActionPending,
0x24 => StatusCode::OperationPending,
0x25 => StatusCode::NoOperations,
0x26 => StatusCode::UnsupportedAlgorithm,
0x27 => StatusCode::OperationDenied,
0x28 => StatusCode::KeyStoreFull,
0x2A => StatusCode::NoOperationPending,
0x2B => StatusCode::UnsupportedOption,
0x2C => StatusCode::InvalidOption,
0x2D => StatusCode::KeepaliveCancel,
0x2E => StatusCode::NoCredentials,
0x2f => StatusCode::UserActionTimeout,
0x30 => StatusCode::NotAllowed,
0x31 => StatusCode::PinInvalid,
0x32 => StatusCode::PinBlocked,
0x33 => StatusCode::PinAuthInvalid,
0x34 => StatusCode::PinAuthBlocked,
0x35 => StatusCode::PinNotSet,
0x36 => StatusCode::PinRequired,
0x37 => StatusCode::PinPolicyViolation,
0x38 => StatusCode::PinTokenExpired,
0x39 => StatusCode::RequestTooLarge,
0x3A => StatusCode::ActionTimeout,
0x3B => StatusCode::UpRequired,
othr => StatusCode::Unknown(othr),
}
}
}
#[cfg(test)]
impl Into<u8> for StatusCode {
fn into(self) -> u8 {
match self {
StatusCode::OK => 0x00,
StatusCode::InvalidCommand => 0x01,
StatusCode::InvalidParameter => 0x02,
StatusCode::InvalidLength => 0x03,
StatusCode::InvalidSeq => 0x04,
StatusCode::Timeout => 0x05,
StatusCode::ChannelBusy => 0x06,
StatusCode::LockRequired => 0x0A,
StatusCode::InvalidChannel => 0x0B,
StatusCode::CBORUnexpectedType => 0x11,
StatusCode::InvalidCBOR => 0x12,
StatusCode::MissingParameter => 0x14,
StatusCode::LimitExceeded => 0x15,
StatusCode::UnsupportedExtension => 0x16,
StatusCode::CredentialExcluded => 0x19,
StatusCode::Processing => 0x21,
StatusCode::InvalidCredential => 0x22,
StatusCode::UserActionPending => 0x23,
StatusCode::OperationPending => 0x24,
StatusCode::NoOperations => 0x25,
StatusCode::UnsupportedAlgorithm => 0x26,
StatusCode::OperationDenied => 0x27,
StatusCode::KeyStoreFull => 0x28,
StatusCode::NoOperationPending => 0x2A,
StatusCode::UnsupportedOption => 0x2B,
StatusCode::InvalidOption => 0x2C,
StatusCode::KeepaliveCancel => 0x2D,
StatusCode::NoCredentials => 0x2E,
StatusCode::UserActionTimeout => 0x2f,
StatusCode::NotAllowed => 0x30,
StatusCode::PinInvalid => 0x31,
StatusCode::PinBlocked => 0x32,
StatusCode::PinAuthInvalid => 0x33,
StatusCode::PinAuthBlocked => 0x34,
StatusCode::PinNotSet => 0x35,
StatusCode::PinRequired => 0x36,
StatusCode::PinPolicyViolation => 0x37,
StatusCode::PinTokenExpired => 0x38,
StatusCode::RequestTooLarge => 0x39,
StatusCode::ActionTimeout => 0x3A,
StatusCode::UpRequired => 0x3B,
StatusCode::Unknown(othr) => othr,
}
}
}
#[derive(Debug)]
pub enum CommandError {
InputTooSmall,
MissingRequiredField(&'static str),
Deserializing(CborError),
Serializing(CborError),
StatusCode(StatusCode, Option<Value>),
Json(json::Error),
Crypto(crypto::CryptoError),
UnsupportedPinProtocol,
}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"),
CommandError::MissingRequiredField(field) => {
write!(f, "CommandError: Missing required field {}", field)
}
CommandError::Deserializing(ref e) => {
write!(f, "CommandError: Error while parsing: {}", e)
}
CommandError::Serializing(ref e) => {
write!(f, "CommandError: Error while serializing: {}", e)
}
CommandError::StatusCode(ref code, ref value) => {
write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value)
}
CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e),
CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e),
CommandError::UnsupportedPinProtocol => {
write!(f, "CommandError: Pin protocol is not supported")
}
}
}
}
impl StdErrorT for CommandError {}
pub(crate) fn calculate_pin_auth<Dev>(
dev: &mut Dev,
client_data_hash: &ClientDataHash,
pin: &Option<Pin>,
) -> Result<Option<PinAuth>, AuthenticatorError>
where
Dev: FidoDevice,
{
let (shared_secret, info) = dev.establish_shared_secret()?;
let pin_auth = if info.options.client_pin == Some(true) {
let pin = pin
.as_ref()
.ok_or(HIDError::Command(CommandError::StatusCode(
StatusCode::PinRequired,
None,
)))?;
let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?;
let pin_token = dev.send_cbor(&pin_command)?;
Some(
pin_token
.auth(client_data_hash.as_ref())
.map_err(CommandError::Crypto)?,
)
} else {
None
};
Ok(pin_auth)
}