/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret}; use crate::ctap2::commands::client_pin::{ ClientPIN, ClientPinResponse, GetKeyAgreement, GetPinToken, GetPinUvAuthTokenUsingPinWithPermissions, GetPinUvAuthTokenUsingUvWithPermissions, PinUvAuthTokenPermission, }; use crate::ctap2::commands::credential_management::{ CredentialManagement, CredentialManagementResponse, }; 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; pub use crate::transport::device_selector::BlinkResult; use crate::transport::errors::HIDError; use crate::{Pin, StatusUpdate}; use std::convert::TryFrom; use std::fmt; use std::sync::mpsc::Sender; 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 CtapVersionSupport { /// `true` if the device supports CTAP1/U2F commands, according to the init message. /// /// Returns `false` if the device has not sent an init message. fn supports_ctap1(&self) -> bool; /// `true` if the device supports CTAP2 commands, according to the init message. /// /// Returns `false` if the device has not sent an init message. fn supports_ctap2(&self) -> bool; } pub trait FidoDeviceIO { fn send_msg + RequestCtap2>( &mut self, msg: &Req, status: Option<&Sender>, ) -> Result { self.send_msg_cancellable(msg, &|| true, status) } fn send_cbor( &mut self, msg: &Req, status: Option<&Sender>, ) -> Result { self.send_cbor_cancellable(msg, &|| true, status) } fn send_ctap1( &mut self, msg: &Req, status: Option<&Sender>, ) -> Result { self.send_ctap1_cancellable(msg, &|| true, status) } fn send_msg_cancellable + RequestCtap2>( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, status: Option<&Sender>, ) -> Result; fn send_cbor_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, status: Option<&Sender>, ) -> Result; fn send_ctap1_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, status: Option<&Sender>, ) -> 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 + CtapVersionSupport 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; #[deprecated = "use CtapVersionSupport::supports_ctap2"] fn should_try_ctap2(&self) -> bool { self.supports_ctap2() } 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, None) { debug!("Refreshed authenticator info: {:?}", info); self.set_authenticator_info(info); } self.get_authenticator_info() } /// Indicates whether we're using CTAP1 (U2F) or CTAP2. /// /// Prior to initializing the device, this returns CTAP2. Initialization checks the CBOR /// capability, and automatically downgrades to CTAP1 if that's not supported. fn get_protocol(&self) -> FidoProtocol; /// Downgrades the connection to CTAP1/U2F. /// /// Returns [`HIDError::UnexpectedVersion`] if the authenticator does not support CTAP1. /// /// 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) -> Result<(), HIDError>; 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.supports_ctap2() { let command = GetInfo::default(); if let Ok(info) = self.send_cbor(&command, None) { debug!("{:?}", info); if info.max_supported_version() == AuthenticatorVersion::U2F_V2 { self.downgrade_to_ctap1()?; } self.set_authenticator_info(info); return Ok(()); } } // If the device sets NMSG, this will fail. 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, None)?; 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() .is_some_and(|i| i.versions.contains(&AuthenticatorVersion::FIDO_2_1)); let resp = if supports_select_cmd { let msg = Selection {}; self.send_cbor_cancellable(&msg, keep_alive, None) } 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, None) .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, None)?; 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, None)?; 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, None)?; 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, None)?; 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>; fn manage_credentials( &mut self, req: &CredentialManagement, ) -> Result; }