diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index 61705ca..2b2c0eb 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -15,6 +15,13 @@ path = "src/lib.rs" [features] default = [] +nfc = ["apdu-core", "apdu"] +pcsc = [ "nfc", "dep:pcsc" ] +libnfc = [ + "nfc", + "nfc1-sys", + "nfc1", +] [dependencies] base64-url = "3.0.0" @@ -64,6 +71,11 @@ snow = { version = "0.10", features = ["use-p256"] } ctap-types = { version = "0.4.0" } btleplug = "0.11.7" thiserror = "2.0.12" +apdu-core = { version = "0.4.0", optional = true } +apdu = { version = "0.4.0", optional = true } +pcsc = { version = "2.9.0", optional = true } +nfc1 = { version = "0.6.0", optional = true, default-features = false } +nfc1-sys = { version = "0.3.9", optional = true, default-features = false } [dev-dependencies] tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } diff --git a/libwebauthn/examples/u2f_hid.rs b/libwebauthn/examples/u2f_hid.rs index 58be0e2..1026ff3 100644 --- a/libwebauthn/examples/u2f_hid.rs +++ b/libwebauthn/examples/u2f_hid.rs @@ -6,7 +6,10 @@ use tokio::sync::broadcast::Receiver; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; +#[cfg(not(feature = "nfc"))] use libwebauthn::transport::hid::list_devices; +#[cfg(feature = "nfc")] +use libwebauthn::transport::nfc::list_devices; use libwebauthn::transport::{Channel as _, Device}; use libwebauthn::u2f::U2F; diff --git a/libwebauthn/examples/webauthn_hid.rs b/libwebauthn/examples/webauthn_hid.rs index b5bdb48..d2aadcb 100644 --- a/libwebauthn/examples/webauthn_hid.rs +++ b/libwebauthn/examples/webauthn_hid.rs @@ -17,7 +17,10 @@ use libwebauthn::proto::ctap2::{ Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, }; +#[cfg(not(feature = "nfc"))] use libwebauthn::transport::hid::list_devices; +#[cfg(feature = "nfc")] +use libwebauthn::transport::nfc::list_devices; use libwebauthn::transport::{Channel as _, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; diff --git a/libwebauthn/examples/webauthn_preflight_hid.rs b/libwebauthn/examples/webauthn_preflight_hid.rs index bc54eee..7daf9fe 100644 --- a/libwebauthn/examples/webauthn_preflight_hid.rs +++ b/libwebauthn/examples/webauthn_preflight_hid.rs @@ -20,7 +20,10 @@ use libwebauthn::proto::ctap2::{ Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, }; +#[cfg(not(feature = "nfc"))] use libwebauthn::transport::hid::list_devices; +#[cfg(feature = "nfc")] +use libwebauthn::transport::nfc::list_devices; use libwebauthn::transport::{Channel as _, Device}; use libwebauthn::webauthn::{CtapError, Error as WebAuthnError, WebAuthn}; @@ -156,7 +159,7 @@ pub async fn main() -> Result<(), Box> { } async fn make_credential_call( - channel: &mut HidChannel<'_>, + channel: &mut impl Channel, user_id: &[u8], exclude_list: Option>, ) -> Result { @@ -194,7 +197,7 @@ async fn make_credential_call( } async fn get_assertion_call( - channel: &mut HidChannel<'_>, + channel: &mut impl Channel, allow_list: Vec, ) -> Result { let challenge: [u8; 32] = thread_rng().gen(); diff --git a/libwebauthn/src/transport/mod.rs b/libwebauthn/src/transport/mod.rs index 63ed5f3..aa72f66 100644 --- a/libwebauthn/src/transport/mod.rs +++ b/libwebauthn/src/transport/mod.rs @@ -6,6 +6,8 @@ pub mod device; pub mod hid; #[cfg(test)] pub mod virt; +#[cfg(feature = "nfc")] +pub mod nfc; mod channel; mod transport; diff --git a/libwebauthn/src/transport/nfc/channel.rs b/libwebauthn/src/transport/nfc/channel.rs new file mode 100644 index 0000000..a7a0f39 --- /dev/null +++ b/libwebauthn/src/transport/nfc/channel.rs @@ -0,0 +1,320 @@ +use apdu::core::HandleError; +use apdu::{Command, Response, command}; +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::{Level, debug, instrument, trace, warn}; + +use crate::webauthn::error::Error; +use crate::UvUpdate; +use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; +use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; +use crate::transport::channel::{AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore}; +use crate::transport::device::SupportedProtocols; +use crate::transport::error::TransportError; + +use super::commands::{command_ctap_msg, command_get_response}; + +const SELECT_P1: u8 = 0x04; +const SELECT_P2: u8 = 0x00; +const APDU_FIDO: &[u8; 8] = b"\xa0\x00\x00\x06\x47\x2f\x00\x01"; +const SW1_MORE_DATA: u8 = 0x61; + +pub type CancelNfcOperation = (); + +#[derive(thiserror::Error)] +pub enum NfcError { + /// APDU error returned by the card. + Apdu(#[from] apdu::Error), + + /// Unexpected error occurred on the device. + Device(Box), +} + +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) => e.fmt(f), + } + } +} + +impl From 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) + } +} + +pub trait HandlerInCtx { + /// Handles the APDU command in a specific context. + /// Implementations must transmit the command to the card through a reader, + /// then receive the response from them, returning length of the data written. + fn handle_in_ctx(&mut self, ctx: Ctx, command: &[u8], response: &mut [u8]) + -> apdu_core::Result; +} + +pub trait NfcBackend: HandlerInCtx + Display {} + +#[derive(Debug, Clone)] +pub struct NfcChannelHandle { + tx: Sender, +} + +impl NfcChannelHandle { + pub async fn cancel_ongoing_operation(&self) { + let _ = self.tx.send(()).await; + } +} + +pub struct NfcChannel +where + Ctx: Copy + Sync, +{ + delegate: Box + Send + Sync>, + auth_token_data: Option, + ux_update_sender: broadcast::Sender, + handle: NfcChannelHandle, + ctx: Ctx, + _apdu_response: Option, + cbor_response: Option, + supported: SupportedProtocols, + status: ChannelStatus, +} + +impl Display for NfcChannel +where + Ctx: Copy + Send + Sync, +{ + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "{}", self.delegate) + } +} + +impl NfcChannel +where + Ctx: fmt::Debug + Display + Copy + Send + Sync, +{ + pub fn new( + delegate: Box + 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 { + warn!("WINK capability is not supported"); + return Ok(false); + } + + pub fn select_fido2(&mut self) -> Result<(), Error> { + let command = command::select_file(SELECT_P1, SELECT_P2, APDU_FIDO); + let is_u2f_v2 = self.handle(self.ctx, command).map(|e| (e == b"U2F_V2"))?; + self.supported = SupportedProtocols { + u2f: is_u2f_v2, + // A CTAP authenticatorGetInfo should be issued to + // determine if the device supports CTAP2 or + // not. Assume it does for now. + fido2: true, + }; + + Ok(()) + } + + fn handle_in_ctx( + &mut self, + ctx: Ctx, + command_buf: &Vec, + buf: &mut [u8], + ) -> Result { + self.delegate + .handle_in_ctx(ctx, &command_buf, buf) + .map_err(|e| match e { + HandleError::NotEnoughBuffer(l) => { + NfcError::Device(Box::new(HandleError::NotEnoughBuffer(l))) + } + HandleError::Nfc(e) => NfcError::Device(e), + }) + } + + pub fn handle<'a>( + &'a mut self, + ctx: Ctx, + command: impl Into>, + ) -> Result, NfcError> { + let command = command.into(); + let command_buf = Vec::from(command); + + let mut buf = [0u8; 1024]; + let mut rapdu = Vec::new(); + + let len: usize = self.handle_in_ctx(ctx, &command_buf, &mut buf)? as usize; + let mut resp = Response::from(&buf[..len]); + + 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)?; + resp = Response::from(&buf[..len]); + (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() + }) + } +} + +#[async_trait] +impl<'a, Ctx> Channel for NfcChannel +where + Ctx: Copy + Send + Sync + fmt::Debug + Display, +{ + type UxUpdate = UvUpdate; + + async fn supported_protocols(&self) -> Result { + Ok(self.supported) + } + + async fn status(&self) -> ChannelStatus { + self.status + } + + async fn close(&mut self) { + todo!("close") + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn apdu_send(&self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + todo!("apdu_send") + } + + #[instrument(level = Level::DEBUG, skip_all)] + async fn apdu_recv(&self, _timeout: Duration) -> Result { + todo!("apdu_recv") + } + + #[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 = &rest[..250]; + rest = &rest[250..]; + 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); + + // FIXME check for SW_UPDATE? + + // let mut rapdu_buf = [0; pcsc::MAX_BUFFER_SIZE_EXTENDED]; + // let (mut resp, mut sw1, mut sw2) = self.card + // .chain_apdus(0x80, 0x10, 0x80, 0x00, data, &mut rapdu_buf) + // .expect("APDU exchange failed"); + + // loop { + // while (sw1, sw2) == SW_UPDATE { + // // ka_status = STATUS(resp[0]) + // // if on_keepalive and last_ka != ka_status: + // // last_ka = ka_status + // // on_keepalive(ka_status) + // // NFCCTAP_GETRESPONSE + + // (resp, sw1, sw2) = self.card + // .chain_apdus(0x80, 0x11, 0x00, 0x00, &[], &mut rapdu_buf).expect("APDU chained exchange failed"); + // debug!("Error {:?} {:?}", sw1, sw2); + // } + + // if (sw1, sw2) != SW_SUCCESS { + // return Err(Error::Transport(TransportError::InvalidFraming)); + // } + + 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 { + self.cbor_response + .take() + .ok_or(Error::Transport(TransportError::InvalidFraming)) + } + + fn get_ux_update_sender(&self) -> &broadcast::Sender { + &self.ux_update_sender + } +} + +impl Ctap2AuthTokenStore for NfcChannel +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; + } +} diff --git a/libwebauthn/src/transport/nfc/commands.rs b/libwebauthn/src/transport/nfc/commands.rs new file mode 100644 index 0000000..50a35e9 --- /dev/null +++ b/libwebauthn/src/transport/nfc/commands.rs @@ -0,0 +1,87 @@ +use apdu::Command; + +// Copy private impl +const CLA_DEFAULT: u8 = 0x00; +const CLA_INTER_INDUSTRY: u8 = 0x80; + +macro_rules! impl_into_vec { + ($name: ty) => { + impl<'a> From<$name> for Vec { + fn from(cmd: $name) -> Self { + Command::from(cmd).into() + } + } + }; +} + +const INS_GET_RESPONSE: u8 = 0xC0; + +/// `GET RESPONSE` (0xC0) command. +#[derive(Debug)] +pub struct GetResponseCommand { + p1: u8, + p2: u8, + le: u8, +} + +impl GetResponseCommand { + /// Constructs a `GET RESPONSE` command. + pub fn new(p1: u8, p2: u8, le: u8) -> Self { + Self { p1, p2, le } + } +} + +impl<'a> From for Command<'a> { + fn from(cmd: GetResponseCommand) -> Self { + Self::new_with_le(CLA_DEFAULT, INS_GET_RESPONSE, cmd.p1, cmd.p2, cmd.le.into()) + } +} + +impl_into_vec!(GetResponseCommand); + +/// Constructs a `GET RESPONSE` command. +pub fn command_get_response(p1: u8, p2: u8, le: u8) -> GetResponseCommand { + GetResponseCommand::new(p1, p2, le) +} + +const CLA_HAS_MORE: u8 = 0x10; +const INS_CTAP_MSG: u8 = 0x10; +const _CTAP_P1_SUPP_GET_RESP: u8 = 0x80; +const CTAP_P2: u8 = 0x00; + +/// `CTAP MSG` (0x10) command. +#[derive(Debug)] +pub struct CtapMsgCommand<'a> { + has_more: bool, + payload: &'a [u8], +} + +impl<'a> CtapMsgCommand<'a> { + /// Constructs a `CTAP MSG` command. + pub fn new(has_more: bool, payload: &'a [u8]) -> Self { + Self { has_more, payload } + } +} + +impl<'a> From> for Command<'a> { + fn from(cmd: CtapMsgCommand<'a>) -> Self { + let cla = match cmd.has_more { + true => CLA_HAS_MORE, + false => 0, + } | CLA_INTER_INDUSTRY; + Self::new_with_payload( + cla, + INS_CTAP_MSG, + 0, //CTAP_P1_SUPP_GET_RESP, + CTAP_P2, + cmd.payload, + ) + } +} + +impl_into_vec!(CtapMsgCommand<'a>); + +/// Constructs a `GET MSG` command. +pub fn command_ctap_msg(has_more: bool, payload: &[u8]) -> CtapMsgCommand { + CtapMsgCommand::new(has_more, payload) +} diff --git a/libwebauthn/src/transport/nfc/device.rs b/libwebauthn/src/transport/nfc/device.rs new file mode 100644 index 0000000..d0c8a8e --- /dev/null +++ b/libwebauthn/src/transport/nfc/device.rs @@ -0,0 +1,123 @@ +use async_trait::async_trait; +use std::fmt; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +use crate::webauthn::error::Error; +use crate::transport::device::Device; + +use super::channel::NfcChannel; +#[cfg(feature = "libnfc")] +use super::libnfc; +#[cfg(feature = "pcsc")] +use super::pcsc; +use super::{Context, Nfc}; + +#[derive(Debug)] +enum DeviceInfo { + #[cfg(feature = "libnfc")] + LibNfc(libnfc::Info), + #[cfg(feature = "pcsc")] + Pcsc(pcsc::Info), +} + +#[derive(Debug)] +pub struct NfcDevice { + info: DeviceInfo, +} + +impl fmt::Display for DeviceInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + #[cfg(feature = "libnfc")] + DeviceInfo::LibNfc(info) => write!(f, "{}", info), + #[cfg(feature = "pcsc")] + DeviceInfo::Pcsc(info) => write!(f, "{}", info), + } + } +} + +impl fmt::Display for NfcDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.info) + } +} + +impl NfcDevice { + #[cfg(feature = "libnfc")] + pub fn new_libnfc(info: libnfc::Info) -> Self { + NfcDevice { + info: DeviceInfo::LibNfc(info), + } + } + + #[cfg(feature = "pcsc")] + pub fn new_pcsc(info: pcsc::Info) -> Self { + NfcDevice { + info: DeviceInfo::Pcsc(info), + } + } + + fn channel_sync<'d>( + &'d self, + ) -> Result, Error> { + trace!("nfc channel {:?}", self); + let mut channel: NfcChannel = match &self.info + { + #[cfg(feature = "libnfc")] + DeviceInfo::LibNfc(info) => info.channel(), + #[cfg(feature = "pcsc")] + DeviceInfo::Pcsc(info) => info.channel(), + }?; + + channel.select_fido2()?; + + Ok(channel) + } +} + +#[async_trait] +impl<'d> Device<'d, Nfc, NfcChannel> for NfcDevice { + async fn channel( + &'d mut self, + ) -> Result, Error> { + self.channel_sync() + } +} + +fn is_fido(device: &NfcDevice) -> bool +where + Ctx: fmt::Debug + fmt::Display + Copy + Send + Sync, +{ + fn inner(device: &NfcDevice) -> Result + where + Ctx: fmt::Debug + fmt::Display + Copy + Send + Sync, + { + let mut chan = device.channel_sync()?; + let _ = chan.select_fido2()?; + Ok(true) + } + + inner::(device).is_ok() +} + +#[instrument] +pub async fn list_devices() -> Result, Error> { + let mut all_devices = Vec::new(); + let list_devices_fns = [ + #[cfg(feature = "libnfc")] + libnfc::list_devices, + #[cfg(feature = "pcsc")] + pcsc::list_devices, + ]; + + for list_devices in list_devices_fns { + let mut devices = list_devices()? + .into_iter() + .filter(|e| is_fido::(&e)) + .collect::>(); + all_devices.append(&mut devices); + } + + Ok(all_devices) +} diff --git a/libwebauthn/src/transport/nfc/libnfc/mod.rs b/libwebauthn/src/transport/nfc/libnfc/mod.rs new file mode 100644 index 0000000..26fb794 --- /dev/null +++ b/libwebauthn/src/transport/nfc/libnfc/mod.rs @@ -0,0 +1,230 @@ +use super::Context; +use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; +use super::device::NfcDevice; +use crate::webauthn::error::Error; +use crate::transport::error::TransportError; +use apdu::core::HandleError; +use apdu_core; +use std::fmt; +use std::fmt::Debug; +use std::io::Write; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +const MAX_DEVICES: usize = 10; +const TIMEOUT: Duration = Duration::from_millis(5000); +const MODULATION_TYPE: nfc1::ModulationType = nfc1::ModulationType::Iso14443a; + +#[derive(Debug)] +pub struct Info { + connstring: String, +} + +impl fmt::Display for Info { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.connstring) + } +} + +fn map_error(_err: nfc1::Error) -> Error { + Error::Transport(TransportError::ConnectionFailed) +} + +impl From for Error { + fn from(input: nfc1::Error) -> Self { + trace!("{:?}", input); + let output = match input { + // rs-nfc1 errors + nfc1::Error::Malloc => TransportError::TransportUnavailable, + nfc1::Error::Undefined(_c_int) => TransportError::TransportUnavailable, + nfc1::Error::UndefinedModulationType => TransportError::TransportUnavailable, + nfc1::Error::NoDeviceFound => TransportError::TransportUnavailable, + + // libnfc errors + nfc1::Error::Io => TransportError::ConnectionLost, + nfc1::Error::InvalidArgument => TransportError::NegotiationFailed, + nfc1::Error::DeviceNotSupported => TransportError::InvalidEndpoint, + nfc1::Error::NoSuchDeviceFound => TransportError::InvalidEndpoint, + nfc1::Error::BufferOverflow => TransportError::InvalidFraming, + nfc1::Error::Timeout => TransportError::Timeout, + nfc1::Error::OperationAborted => TransportError::InvalidFraming, + nfc1::Error::NotImplemented => TransportError::NegotiationFailed, + nfc1::Error::TargetReleased => TransportError::NegotiationFailed, + nfc1::Error::RfTransmissionError => TransportError::NegotiationFailed, + nfc1::Error::MifareAuthFailed => TransportError::NegotiationFailed, + nfc1::Error::Soft => TransportError::Timeout, + nfc1::Error::Chip => TransportError::InvalidFraming, + }; + Error::Transport(output) + } +} + +impl Info { + pub fn new(connstring: &String) -> Self { + Info { + connstring: connstring.clone(), + } + } + + pub fn channel(&self) -> Result, Error> { + let context = nfc1::Context::new().map_err(|e| map_error(e))?; + + let mut chan = Channel::new(self, context); + + { + let mut device = chan.device.lock().unwrap(); + device.initiator_init()?; + device.set_property_bool(nfc1::Property::InfiniteSelect, false)?; + + let info = device.get_information_about()?; + debug!("Info: {}", info); + } + + let target = chan.connect_to_target()?; + debug!("Selected: {:?}", target); + + let ctx = Context {}; + let channel = NfcChannel::new(Box::new(chan), ctx); + Ok(channel) + } +} + +pub struct Channel { + device: Arc>, +} + +unsafe impl Send for Channel {} + +impl Channel { + pub fn new(info: &Info, mut context: nfc1::Context) -> Self { + let device = context + .open_with_connstring(&info.connstring) + .expect("opened device"); + + Self { + device: Arc::new(Mutex::new(device)), + } + } + + fn initiator_select_passive_target_ex( + device: &mut nfc1::Device, + modulation: &nfc1::Modulation, + ) -> nfc1::Result { + match device.initiator_select_passive_target(&modulation) { + Ok(target) => { + if let nfc1::target_info::TargetInfo::Iso14443a(iso) = target.target_info { + if iso.uid_len > 0 { + Ok(target) + } else { + Err(nfc1::Error::NoDeviceFound) + } + } else { + Err(nfc1::Error::NoDeviceFound) + } + } + Err(err) => { + println!("Error: {}", err); + Err(err) + } + } + } + + fn connect_to_target(&mut self) -> Result { + let mut device = self.device.lock().unwrap(); + // Assume baudrates are already sorted higher to lower + let baudrates = device.get_supported_baud_rate(nfc1::Mode::Initiator, MODULATION_TYPE)?; + let modulations = baudrates + .iter() + .map(|baud_rate| nfc1::Modulation { + modulation_type: MODULATION_TYPE, + baud_rate: *baud_rate, + }) + .collect::>(); + let modulation = &modulations[modulations.len() - 1]; + let is_one_rate = modulations.len() == 1; + for i in 0..2 { + if i > 0 { + thread::sleep(Duration::from_millis(100)); + } + trace!("Poll {:?} {}", modulation, i); + if let Ok(target) = + Channel::initiator_select_passive_target_ex(&mut device, &modulation) + { + if is_one_rate { + return Ok(target); + } + + for modulation in modulations.iter() { + device.initiator_deselect_target()?; + device.initiator_init()?; + trace!("Try {:?}", modulation); + if let Ok(target) = + Channel::initiator_select_passive_target_ex(&mut device, &modulation) + { + return Ok(target); + } + } + } + } + + Err(Error::Transport(TransportError::TransportUnavailable)) + } +} + +impl HandlerInCtx for Channel +where + Ctx: fmt::Debug + fmt::Display, +{ + fn handle_in_ctx( + &mut self, + _ctx: Ctx, + command: &[u8], + mut response: &mut [u8], + ) -> apdu_core::Result { + let timeout = nfc1::Timeout::Duration(TIMEOUT); + let len = response.len(); + trace!("TX: {:?}", command); + let rapdu = self + .device + .lock() + .unwrap() + .initiator_transceive_bytes(command, len, timeout) + .map_err(|e| HandleError::Nfc(Box::new(e)))?; + + trace!("RX: {:?}", rapdu); + + if response.len() < rapdu.len() { + return Err(HandleError::NotEnoughBuffer(rapdu.len())); + } + + response + .write(&rapdu) + .map_err(|e| HandleError::Nfc(Box::new(e))) + } +} + +impl fmt::Display for Channel { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + let mut device = self.device.lock().unwrap(); + write!(f, "{}", device.name()) + } +} + +impl NfcBackend for Channel where Ctx: fmt::Debug + fmt::Display {} + +#[instrument] +pub fn list_devices() -> Result, Error> { + let mut context = + nfc1::Context::new().map_err(|_| Error::Transport(TransportError::TransportUnavailable))?; + let devices = context + .list_devices(MAX_DEVICES) + .expect("libnfc devices") + .iter() + .map(|x| NfcDevice::new_libnfc(Info::new(x))) + .collect::>(); + + Ok(devices) +} diff --git a/libwebauthn/src/transport/nfc/mod.rs b/libwebauthn/src/transport/nfc/mod.rs new file mode 100644 index 0000000..cc94bb6 --- /dev/null +++ b/libwebauthn/src/transport/nfc/mod.rs @@ -0,0 +1,37 @@ +use std::fmt::{Display, Formatter}; + +pub mod channel; +pub mod commands; +pub mod device; +#[cfg(feature = "libnfc")] +pub mod libnfc; +#[cfg(feature = "pcsc")] +pub mod pcsc; + +pub use device::list_devices; + +use super::Transport; + +pub struct Nfc {} +impl Transport for Nfc {} +unsafe impl Send for Nfc {} +unsafe impl Sync for Nfc {} + +impl Display for Nfc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NFC") + } +} + +#[derive(Clone, Debug)] +pub struct Context {} + +impl Display for Context { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "context") + } +} + +unsafe impl Send for Context {} +unsafe impl Sync for Context {} +impl Copy for Context {} diff --git a/libwebauthn/src/transport/nfc/pcsc/mod.rs b/libwebauthn/src/transport/nfc/pcsc/mod.rs new file mode 100644 index 0000000..d0b0aa9 --- /dev/null +++ b/libwebauthn/src/transport/nfc/pcsc/mod.rs @@ -0,0 +1,165 @@ +use super::Context; +use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; +use super::device::NfcDevice; +use crate::webauthn::error::Error; +use crate::transport::error::TransportError; +use apdu::core::HandleError; +use pcsc; +use std::ffi::{CStr, CString}; +use std::fmt; +use std::fmt::Debug; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +#[allow(unused_imports)] +use tracing::{debug, info, instrument, trace}; + +#[derive(Debug)] +pub struct Info { + name: CString, +} + +pub struct PcscCard { + pub card: Option, +} + +impl<'tx> Deref for PcscCard { + type Target = pcsc::Card; + + fn deref(&self) -> &pcsc::Card { + self.card.as_ref().unwrap() + } +} + +// By default pcsc resets the card but to be able to reconnect the +// card has to be powered down instead. +impl Drop for PcscCard { + fn drop(&mut self) { + let _ = PcscCard::disconnect(self.card.take()); + } +} + +impl PcscCard { + pub fn new(card: pcsc::Card) -> Self { + PcscCard { card: Some(card) } + } + + fn map_disconnect_error(pair: (pcsc::Card, pcsc::Error)) -> Error { + let (_card, _err) = pair; + Error::Transport(TransportError::InvalidFraming) + } + + fn disconnect(card: Option) -> Result<(), Error> { + match card { + Some(card) => { + debug!("Disconnect card"); + card.disconnect(pcsc::Disposition::UnpowerCard) + .map_err(PcscCard::map_disconnect_error) + } + None => Ok(()), + } + } +} + +pub struct Channel { + card: Arc>, +} + +unsafe impl Send for Channel {} + +impl fmt::Display for Info { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.name) + } +} + +impl From for Error { + fn from(input: pcsc::Error) -> Self { + trace!("{:?}", input); + let output = match input { + pcsc::Error::NoSmartcard => TransportError::ConnectionFailed, + _ => TransportError::InvalidFraming, + }; + + Error::Transport(output) + } +} + +impl Info { + pub fn new(name: &CStr) -> Self { + Info { + name: CStr::into_c_string(name.into()), + } + } + + pub fn channel(&self) -> Result, Error> { + let context = pcsc::Context::establish(pcsc::Scope::User)?; + let chan = Channel::new(self, context)?; + + let ctx = Context {}; + let channel = NfcChannel::new(Box::new(chan), ctx); + Ok(channel) + } +} + +impl Channel { + pub fn new(info: &Info, context: pcsc::Context) -> Result { + let card = context.connect(&info.name, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)?; + + let chan = Self { + card: Arc::new(Mutex::new(PcscCard::new(card))), + }; + + Ok(chan) + } +} + +impl fmt::Display for Channel { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + let card = self.card.lock().unwrap(); + let (names_len, atr_len) = card.status2_len().unwrap(); + let mut names_buf = vec![0; names_len]; + let mut atr_buf = vec![0; atr_len]; + let status = card.status2(&mut names_buf, &mut atr_buf).unwrap(); + write!(f, "{:?}", status.reader_names().collect::>()) + } +} + +impl NfcBackend for Channel where Ctx: fmt::Debug + fmt::Display {} + +impl HandlerInCtx for Channel +where + Ctx: fmt::Debug + fmt::Display, +{ + fn handle_in_ctx( + &mut self, + _ctx: Ctx, + command: &[u8], + response: &mut [u8], + ) -> apdu_core::Result { + trace!("TX: {:?}", command); + + let rapdu = self + .card + .lock() + .unwrap() + .transmit(command, response) + .map_err(|e| HandleError::Nfc(Box::new(e)))?; + + trace!("RX: {:?}", rapdu); + Ok(rapdu.len()) + } +} + +#[instrument] +pub fn list_devices() -> Result, Error> { + let ctx = pcsc::Context::establish(pcsc::Scope::User).expect("PC/SC context"); + let len = ctx.list_readers_len().expect("PC/SC readers len"); + let mut readers_buf = vec![0; len]; + let devices = ctx + .list_readers(&mut readers_buf) + .expect("PC/SC readers") + .map(|x| NfcDevice::new_pcsc(Info::new(x))) + .collect::>(); + + Ok(devices) +}