use apdu::core::HandleError;
use apdu::{command, Command, Response};
use apdu_core;
use async_trait::async_trait;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use std::time::Duration;
use tokio::sync::broadcast;
use tokio::sync::mpsc::{self, Sender};
#[allow(unused_imports)]
use tracing::{debug, instrument, trace, warn, Level};
use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse};
use crate::proto::ctap2::cbor::{CborRequest, CborResponse};
use crate::proto::ctap2::Ctap2;
use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore};
use crate::transport::device::SupportedProtocols;
use crate::transport::error::TransportError;
use crate::webauthn::Error;
use crate::UvUpdate;
use super::commands::{command_ctap_msg, command_get_response};
const SELECT_P1: u8 = 0x04;
const SELECT_P2: u8 = 0x00;
const FIDO2_AID: &[u8; 8] = b"\xa0\x00\x00\x06\x47\x2f\x00\x01";
const SW1_MORE_DATA: u8 = 0x61;
fn is_fido2_version(version: &[u8]) -> bool {
matches!(
version,
b"FIDO_2_0" | b"FIDO_2_1_PRE" | b"FIDO_2_1" | b"FIDO_2_2"
)
}
pub type CancelNfcOperation = ();
#[derive(thiserror::Error)]
pub enum NfcError {
Apdu(#[from] apdu::Error),
Device(#[from] HandleError),
}
impl From<NfcError> for Error {
fn from(input: NfcError) -> Self {
trace!("{:?}", input);
let output = match input {
NfcError::Apdu(_apdu_error) => TransportError::InvalidFraming,
NfcError::Device(_) => TransportError::ConnectionLost,
};
Error::Transport(output)
}
}
impl Debug for NfcError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
}
impl Display for NfcError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
NfcError::Apdu(e) => Display::fmt(e, f),
NfcError::Device(e) => Display::fmt(e, f),
}
}
}
pub trait HandlerInCtx<Ctx> {
fn handle_in_ctx(&mut self, ctx: Ctx, command: &[u8], response: &mut [u8])
-> apdu_core::Result;
}
pub trait NfcBackend<Ctx>: HandlerInCtx<Ctx> + Display {}
#[derive(Debug, Clone)]
pub struct NfcChannelHandle {
tx: Sender<CancelNfcOperation>,
}
impl NfcChannelHandle {
pub async fn cancel_ongoing_operation(&self) {
let _ = self.tx.send(()).await;
}
}
pub struct NfcChannel<Ctx>
where
Ctx: Copy + Sync,
{
delegate: Box<dyn NfcBackend<Ctx> + Send + Sync>,
auth_token_data: Option<AuthTokenData>,
ux_update_sender: broadcast::Sender<UvUpdate>,
handle: NfcChannelHandle,
ctx: Ctx,
apdu_response: Option<ApduResponse>,
cbor_response: Option<CborResponse>,
supported: SupportedProtocols,
status: ChannelStatus,
}
impl<Ctx> Display for NfcChannel<Ctx>
where
Ctx: Copy + Send + Sync,
{
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.delegate)
}
}
impl<Ctx> NfcChannel<Ctx>
where
Ctx: fmt::Debug + Display + Copy + Send + Sync,
{
pub fn new(delegate: Box<dyn NfcBackend<Ctx> + Send + Sync>, ctx: Ctx) -> Self {
let (ux_update_sender, _) = broadcast::channel(16);
let (handle_tx, _handle_rx) = mpsc::channel(1);
let handle = NfcChannelHandle { tx: handle_tx };
NfcChannel {
delegate,
auth_token_data: None,
ux_update_sender,
handle,
ctx,
apdu_response: None,
cbor_response: None,
supported: SupportedProtocols {
fido2: false,
u2f: false,
},
status: ChannelStatus::Ready,
}
}
pub fn get_handle(&self) -> NfcChannelHandle {
self.handle.clone()
}
#[instrument(skip_all)]
pub async fn wink(&mut self, _timeout: Duration) -> Result<bool, Error> {
warn!("WINK capability is not supported");
Ok(false)
}
pub async fn select_fido2(&mut self) -> Result<(), Error> {
let command = command::select_file(SELECT_P1, SELECT_P2, FIDO2_AID);
let response = self.handle(self.ctx, command)?;
let mut u2f = false;
let mut fido2 = false;
if is_fido2_version(&response) {
fido2 = true;
} else if response == b"U2F_V2" {
u2f = true;
fido2 = self.ctap2_get_info().await.is_ok();
}
self.supported = SupportedProtocols { u2f, fido2 };
Ok(())
}
fn handle_in_ctx(
&mut self,
ctx: Ctx,
command_buf: &[u8],
buf: &mut [u8],
) -> Result<usize, NfcError> {
let res = self.delegate.handle_in_ctx(ctx, command_buf, buf)?;
Ok(res)
}
pub fn handle<'a>(
&'a mut self,
ctx: Ctx,
command: impl Into<Command<'a>>,
) -> Result<Vec<u8>, NfcError> {
let command = command.into();
let command_buf = Vec::from(command);
let mut buf = [0u8; 1024];
let mut rapdu = Vec::new();
let len = self.handle_in_ctx(ctx, &command_buf, &mut buf)?;
let resp_bytes = buf.get(..len).ok_or(HandleError::NotEnoughBuffer(len))?;
let mut resp = Response::from(resp_bytes);
let (mut sw1, mut sw2) = resp.trailer;
rapdu.extend_from_slice(resp.payload);
while sw1 == SW1_MORE_DATA {
let get_response_cmd = command_get_response(0x00, 0x00, sw2);
let get_response_buf = Vec::from(get_response_cmd);
let len = self.handle_in_ctx(ctx, &get_response_buf, &mut buf)?;
let resp_bytes = buf.get(..len).ok_or(HandleError::NotEnoughBuffer(len))?;
resp = Response::from(resp_bytes);
(sw1, sw2) = resp.trailer;
rapdu.extend_from_slice(resp.payload);
}
rapdu.extend_from_slice(&[sw1, sw2]);
Result::from(Response::from(rapdu.as_slice()))
.map(|p| p.to_vec())
.map_err(|e| {
trace!("map_err {:?}", e);
apdu::Error::from(e).into()
})
}
#[instrument(skip_all)]
pub async fn blink_and_wait_for_user_presence(
&mut self,
_timeout: Duration,
) -> Result<bool, Error> {
unimplemented!()
}
}
#[async_trait]
impl<Ctx> Channel for NfcChannel<Ctx>
where
Ctx: Copy + Send + Sync + fmt::Debug + Display,
{
type UxUpdate = UvUpdate;
async fn supported_protocols(&self) -> Result<SupportedProtocols, Error> {
Ok(self.supported)
}
async fn status(&self) -> ChannelStatus {
self.status
}
async fn close(&mut self) {}
#[instrument(level = Level::DEBUG, skip_all)]
async fn apdu_send(&mut self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> {
let resp = self.handle(self.ctx, request)?;
trace!("apdu_send {:?}", resp);
let apdu_response = ApduResponse::new_success(&resp);
self.apdu_response = Some(apdu_response);
Ok(())
}
#[instrument(level = Level::DEBUG, skip_all)]
async fn apdu_recv(&mut self, _timeout: Duration) -> Result<ApduResponse, Error> {
self.apdu_response
.take()
.ok_or(Error::Transport(TransportError::InvalidFraming))
}
#[instrument(level = Level::DEBUG, skip_all)]
async fn cbor_send(
&mut self,
request: &CborRequest,
_timeout: std::time::Duration,
) -> Result<(), Error> {
let data = &request.ctap_hid_data();
let mut rest: &[u8] = data;
while rest.len() > 250 {
let (to_send, remaining) = rest.split_at(250);
rest = remaining;
let ctap_msg = command_ctap_msg(true, to_send);
let resp = self.handle(self.ctx, ctap_msg)?;
trace!("cbor_send has_more {:?} {:?}", to_send, resp);
}
let ctap_msg = command_ctap_msg(false, rest);
let resp = self.handle(self.ctx, ctap_msg)?;
trace!("cbor_send {:?} {:?}", rest, resp);
let cbor_response = CborResponse::try_from(&resp)
.or(Err(Error::Transport(TransportError::InvalidFraming)))?;
self.cbor_response = Some(cbor_response);
Ok(())
}
#[instrument(level = Level::DEBUG, skip_all)]
async fn cbor_recv(&mut self, _timeout: std::time::Duration) -> Result<CborResponse, Error> {
self.cbor_response
.take()
.ok_or(Error::Transport(TransportError::InvalidFraming))
}
fn get_ux_update_sender(&self) -> &broadcast::Sender<UvUpdate> {
&self.ux_update_sender
}
}
impl<Ctx> Ctap2AuthTokenStore for NfcChannel<Ctx>
where
Ctx: Copy + Send + Sync,
{
fn store_auth_data(&mut self, auth_token_data: AuthTokenData) {
self.auth_token_data = Some(auth_token_data);
}
fn get_auth_data(&self) -> Option<&AuthTokenData> {
self.auth_token_data.as_ref()
}
fn clear_uv_auth_token_store(&mut self) {
self.auth_token_data = None;
}
}
#[cfg(test)]
mod tests {
use super::is_fido2_version;
#[test]
fn fido2_versions_are_recognised() {
assert!(is_fido2_version(b"FIDO_2_0"));
assert!(is_fido2_version(b"FIDO_2_1_PRE"));
assert!(is_fido2_version(b"FIDO_2_1"));
assert!(is_fido2_version(b"FIDO_2_2"));
}
#[test]
fn u2f_v2_is_not_classified_as_fido2() {
assert!(!is_fido2_version(b"U2F_V2"));
}
#[test]
fn unknown_versions_are_rejected() {
assert!(!is_fido2_version(b""));
assert!(!is_fido2_version(b"FIDO_2"));
assert!(!is_fido2_version(b"FIDO_2_3"));
assert!(!is_fido2_version(b"FIDO_3_0"));
assert!(!is_fido2_version(b"fido_2_0"));
assert!(!is_fido2_version(b"FIDO_2_0\0"));
}
}