use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret}; use crate::ctap2::commands::client_pin::{ ClientPIN, ClientPinResponse, GetKeyAgreement, GetPinToken, GetPinUvAuthTokenUsingPinWithPermissions, GetPinUvAuthTokenUsingUvWithPermissions, PinUvAuthTokenPermission, }; use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult}; use crate::ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorVersion, GetInfo}; use crate::ctap2::commands::get_version::{GetVersion, U2FInfo}; use crate::ctap2::commands::make_credentials::{ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsResult, }; use crate::ctap2::commands::reset::Reset; use crate::ctap2::commands::selection::Selection; use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, StatusCode}; use crate::ctap2::preflight::CheckKeyHandle; use crate::transport::device_selector::BlinkResult; use crate::transport::errors::HIDError; use crate::Pin; use std::convert::TryFrom; use std::fmt; pub mod device_selector; pub mod errors; pub mod hid; #[cfg(all( any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"), not(test) ))] pub mod hidproto; #[cfg(all(target_os = "linux", not(test)))] #[path = "linux/mod.rs"] pub mod platform; #[cfg(all(target_os = "freebsd", not(test)))] #[path = "freebsd/mod.rs"] pub mod platform; #[cfg(all(target_os = "netbsd", not(test)))] #[path = "netbsd/mod.rs"] pub mod platform; #[cfg(all(target_os = "openbsd", not(test)))] #[path = "openbsd/mod.rs"] pub mod platform; #[cfg(all(target_os = "macos", not(test)))] #[path = "macos/mod.rs"] pub mod platform; #[cfg(all(target_os = "windows", not(test)))] #[path = "windows/mod.rs"] pub mod platform; #[cfg(not(any( target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "macos", target_os = "windows", test )))] #[path = "stub/mod.rs"] pub mod platform; #[cfg(test)] #[path = "mock/mod.rs"] pub mod platform; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FidoProtocol { CTAP1, CTAP2, } pub trait FidoDeviceIO { fn send_msg + RequestCtap2>( &mut self, msg: &Req, ) -> Result { self.send_msg_cancellable(msg, &|| true) } fn send_cbor(&mut self, msg: &Req) -> Result { self.send_cbor_cancellable(msg, &|| true) } fn send_ctap1(&mut self, msg: &Req) -> Result { self.send_ctap1_cancellable(msg, &|| true) } fn send_msg_cancellable + RequestCtap2>( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, ) -> Result; fn send_cbor_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, ) -> Result; fn send_ctap1_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, ) -> Result; } pub trait TestDevice { #[cfg(test)] fn skip_serialization(&self) -> bool; #[cfg(test)] fn send_ctap1_unserialized( &mut self, msg: &Req, ) -> Result; #[cfg(test)] fn send_ctap2_unserialized( &mut self, msg: &Req, ) -> Result; } pub trait FidoDevice: FidoDeviceIO where Self: Sized, Self: fmt::Debug, { fn pre_init(&mut self) -> Result<(), HIDError>; fn initialized(&self) -> bool; // Check if the device is actually a token fn is_u2f(&mut self) -> bool; fn should_try_ctap2(&self) -> bool; fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>; fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); fn refresh_authenticator_info(&mut self) -> Option<&AuthenticatorInfo> { let command = GetInfo::default(); if let Ok(info) = self.send_cbor(&command) { debug!("Refreshed authenticator info: {:?}", info); self.set_authenticator_info(info); } self.get_authenticator_info() } // `get_protocol()` indicates whether we're using CTAP1 or CTAP2. // Prior to initializing the device, `get_protocol()` should return CTAP2 unless // there's a reason to believe that the device does not support CTAP2 (e.g. if // it's a HID device and it does not have the CBOR capability). fn get_protocol(&self) -> FidoProtocol; // We do not provide a generic `set_protocol(..)` function as this would have complicated // interactions with the AuthenticatorInfo state. fn downgrade_to_ctap1(&mut self); fn get_shared_secret(&self) -> Option<&SharedSecret>; fn set_shared_secret(&mut self, secret: SharedSecret); fn init(&mut self) -> Result<(), HIDError> { self.pre_init()?; if self.should_try_ctap2() { let command = GetInfo::default(); if let Ok(info) = self.send_cbor(&command) { debug!("{:?}", info); if info.max_supported_version() == AuthenticatorVersion::U2F_V2 { self.downgrade_to_ctap1(); } self.set_authenticator_info(info); return Ok(()); } } self.downgrade_to_ctap1(); // We want to return an error here if this device doesn't support CTAP1, // so we send a U2F_VERSION command. let command = GetVersion::default(); self.send_ctap1(&command)?; Ok(()) } fn block_and_blink(&mut self, keep_alive: &dyn Fn() -> bool) -> BlinkResult { let supports_select_cmd = self.get_protocol() == FidoProtocol::CTAP2 && self.get_authenticator_info().map_or(false, |i| { i.versions.contains(&AuthenticatorVersion::FIDO_2_1) }); let resp = if supports_select_cmd { let msg = Selection {}; self.send_cbor_cancellable(&msg, keep_alive) } else { // We need to fake a blink-request, because FIDO2.0 forgot to specify one // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential let msg = dummy_make_credentials_cmd(); info!("Trying to blink: {:?}", &msg); // We don't care about the Ok-value, just if it is Ok or not self.send_msg_cancellable(&msg, keep_alive).map(|_| ()) }; match resp { // Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request, // but Yubikeys for example return PinAuthInvalid. A successful return is also possible // for CTAP1-tokens so we catch those here as well. Ok(_) | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _))) | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _))) | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => { BlinkResult::DeviceSelected } // We cancelled the receive, because another device was selected. Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _))) | Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _))) | Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => { // TODO: Repeat the request, if it is a UserActionTimeout? debug!("Device {:?} got cancelled", &self); BlinkResult::Cancelled } // Something unexpected happened, so we assume this device is not usable and // interpreting this equivalent to being cancelled. e => { info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e); BlinkResult::Cancelled } } } fn establish_shared_secret( &mut self, alive: &dyn Fn() -> bool, ) -> Result { // CTAP1 devices don't support establishing a shared secret let info = match (self.get_protocol(), self.get_authenticator_info()) { (FidoProtocol::CTAP2, Some(info)) => info, _ => return Err(HIDError::UnsupportedCommand), }; let pin_protocol = PinUvAuthProtocol::try_from(info)?; // Not reusing the shared secret here, if it exists, since we might start again // with a different PIN (e.g. if the last one was wrong) let pin_command = GetKeyAgreement::new(pin_protocol.clone()); let resp = self.send_cbor_cancellable(&pin_command, alive)?; if let Some(device_key_agreement_key) = resp.key_agreement { let shared_secret = pin_protocol .encapsulate(&device_key_agreement_key) .map_err(CommandError::from)?; self.set_shared_secret(shared_secret.clone()); Ok(shared_secret) } else { Err(HIDError::Command(CommandError::MissingRequiredField( "key_agreement", ))) } } /// CTAP 2.0-only version: /// "Getting pinUvAuthToken using getPinToken (superseded)" fn get_pin_token( &mut self, pin: &Option, alive: &dyn Fn() -> bool, ) -> Result { // Asking the user for PIN before establishing the shared secret let pin = pin .as_ref() .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?; // Not reusing the shared secret here, if it exists, since we might start again // with a different PIN (e.g. if the last one was wrong) let shared_secret = self.establish_shared_secret(alive)?; let pin_command = GetPinToken::new(&shared_secret, pin); let resp = self.send_cbor_cancellable(&pin_command, alive)?; if let Some(encrypted_pin_token) = resp.pin_token { // CTAP 2.1 spec: // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken. let default_permissions = PinUvAuthTokenPermission::default(); let pin_token = shared_secret .decrypt_pin_token(default_permissions, encrypted_pin_token.as_ref()) .map_err(CommandError::from)?; Ok(pin_token) } else { Err(HIDError::Command(CommandError::MissingRequiredField( "pin_token", ))) } } fn get_pin_uv_auth_token_using_uv_with_permissions( &mut self, permission: PinUvAuthTokenPermission, rp_id: Option<&String>, alive: &dyn Fn() -> bool, ) -> Result { // Explicitly not reusing the shared secret here let shared_secret = self.establish_shared_secret(alive)?; let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new( &shared_secret, permission, rp_id.cloned(), ); let resp = self.send_cbor_cancellable(&pin_command, alive)?; if let Some(encrypted_pin_token) = resp.pin_token { let pin_token = shared_secret .decrypt_pin_token(permission, encrypted_pin_token.as_ref()) .map_err(CommandError::from)?; Ok(pin_token) } else { Err(HIDError::Command(CommandError::MissingRequiredField( "pin_token", ))) } } fn get_pin_uv_auth_token_using_pin_with_permissions( &mut self, pin: &Option, permission: PinUvAuthTokenPermission, rp_id: Option<&String>, alive: &dyn Fn() -> bool, ) -> Result { // Asking the user for PIN before establishing the shared secret let pin = pin .as_ref() .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?; // Not reusing the shared secret here, if it exists, since we might start again // with a different PIN (e.g. if the last one was wrong) let shared_secret = self.establish_shared_secret(alive)?; let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new( &shared_secret, pin, permission, rp_id.cloned(), ); let resp = self.send_cbor_cancellable(&pin_command, alive)?; if let Some(encrypted_pin_token) = resp.pin_token { let pin_token = shared_secret .decrypt_pin_token(permission, encrypted_pin_token.as_ref()) .map_err(CommandError::from)?; Ok(pin_token) } else { Err(HIDError::Command(CommandError::MissingRequiredField( "pin_token", ))) } } } pub trait VirtualFidoDevice: FidoDevice { fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError>; fn client_pin(&self, req: &ClientPIN) -> Result; fn get_assertion(&self, req: &GetAssertion) -> Result, HIDError>; fn get_info(&self) -> Result; fn get_version(&self, req: &GetVersion) -> Result; fn make_credentials(&self, req: &MakeCredentials) -> Result; fn reset(&self, req: &Reset) -> Result<(), HIDError>; fn selection(&self, req: &Selection) -> Result<(), HIDError>; }