diff --git a/src-tauri/fido2/src/hid.rs b/src-tauri/fido2/src/hid.rs index b78a6f21..bd7321ee 100644 --- a/src-tauri/fido2/src/hid.rs +++ b/src-tauri/fido2/src/hid.rs @@ -8,7 +8,7 @@ use ctap_hid_fido2::{ fidokey::make_credential::{CredentialSupportedKeyType, MakeCredentialArgsBuilder}, public_key_credential_user_entity::PublicKeyCredentialUserEntity, - FidoKeyHidFactory, LibCfg, + FidoKeyHid, FidoKeyHidFactory, HidInfo, HidParam, LibCfg, }; use tokio_util::sync::CancellationToken; @@ -57,12 +57,26 @@ fn ceremony_error(err: &impl std::fmt::Display) -> Fido2Error { } } -fn open_device() -> Result { +/// The crate's own factory refuses several keys with the same error text as none, so the +/// count is checked here to tell the two apart. +fn single_device(mut devices: Vec) -> Result { + match devices.len() { + 0 => Err(Fido2Error::NoDevice), + 1 => Ok(devices.pop().expect("length checked above").param), + count => { + tracing::debug!("{count} FIDO2 devices connected"); + Err(Fido2Error::MultipleDevices) + } + } +} + +fn open_device() -> Result { + let param = single_device(ctap_hid_fido2::get_fidokey_devices())?; let mut cfg = LibCfg::init(); // Suppress the crate's keep-alive chatter on stdout. cfg.enable_keep_alive_msg = false; - FidoKeyHidFactory::create(&cfg).map_err(|err| { - tracing::debug!("No FIDO2 device: {err}"); + FidoKeyHidFactory::create_by_params(&[param], &cfg).map_err(|err| { + tracing::debug!("Could not open the FIDO2 device: {err}"); Fido2Error::NoDevice }) } @@ -214,6 +228,30 @@ mod tests { assert_eq!(ctap_status(&Rendered("device not found")), None); } + #[test] + fn test_single_device_rejects_none_and_many() { + let device = |path: &str| HidInfo { + pid: 0, + vid: 0, + product_string: String::new(), + info: String::new(), + param: HidParam::Path(path.to_string()), + }; + + assert!(matches!( + single_device(Vec::new()), + Err(Fido2Error::NoDevice) + )); + assert!(matches!( + single_device(vec![device("a")]), + Ok(HidParam::Path(path)) if path == "a" + )); + assert!(matches!( + single_device(vec![device("a"), device("b")]), + Err(Fido2Error::MultipleDevices) + )); + } + #[test] fn test_recognised_statuses_map_to_actionable_errors() { struct Rendered(String); diff --git a/src-tauri/fido2/src/lib.rs b/src-tauri/fido2/src/lib.rs index b3e57073..7c4b003d 100644 --- a/src-tauri/fido2/src/lib.rs +++ b/src-tauri/fido2/src/lib.rs @@ -55,6 +55,9 @@ impl PlatformContext { pub enum Fido2Error { #[error("no security key detected")] NoDevice, + /// The CTAP backend drives one key and cannot pick between several. + #[error("more than one security key is connected")] + MultipleDevices, #[error("the security key timed out waiting to be touched")] Timeout, #[error("the ceremony was cancelled")] diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index cb9208d6..de9ac8b7 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2103,6 +2103,11 @@ impl Drop for CeremonyGuard<'_> { fn fido2_message(err: &Fido2Error, ceremony: &str) -> String { match err { Fido2Error::NoDevice => "No security key detected".to_string(), + // The CTAP backend cannot choose between keys yet. + Fido2Error::MultipleDevices => { + "More than one security key is connected. Leave only one plugged in and try again" + .to_string() + } // The key was blinking for a touch that never came. Fido2Error::Timeout => "Security key timed out waiting to be touched".to_string(), Fido2Error::NoCredentials => {