diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock index 535978d..6aba4ff 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock @@ -1950,7 +1950,7 @@ dependencies = [ [[package]] name = "libwebauthn" version = "0.1.2" -source = "git+https://github.com/linux-credentials/libwebauthn?rev=dc23daed528f512f2bcb61fce9eb6b8ee74066e2#dc23daed528f512f2bcb61fce9eb6b8ee74066e2" +source = "git+https://github.com/linux-credentials/libwebauthn?rev=73970627b81ff775fbd80fb70694bef049ac2c85#73970627b81ff775fbd80fb70694bef049ac2c85" dependencies = [ "aes", "async-trait", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml index 6b643aa..6056028 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml @@ -20,7 +20,7 @@ serde_json = "1.0.140" tracing = "0.1.41" tracing-subscriber = "0.3" zbus = "5.5.0" -libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "dc23daed528f512f2bcb61fce9eb6b8ee74066e2" } +libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "73970627b81ff775fbd80fb70694bef049ac2c85" } async-trait = "0.1.88" tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs index 8e7c1c3..7931ad2 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs @@ -1,9 +1,12 @@ -use std::sync::{Arc, Mutex, OnceLock}; +use std::{ + sync::{Arc, Mutex, OnceLock}, + time::Duration, +}; use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, - transport::Device as _, + transport::{hid::HidDevice, Device as _}, webauthn::{Error as WebAuthnError, WebAuthn}, UxUpdate, }; @@ -62,32 +65,67 @@ impl CredentialService { pub(crate) async fn poll_device_discovery_usb(&mut self) -> Result { debug!("polling for USB status"); - let prev_usb_state = *self.usb_state.lock().await; + let prev_usb_state = self.usb_state.lock().await.clone(); let next_usb_state = match prev_usb_state { UsbState::Idle | UsbState::Waiting => { - let devices = libwebauthn::transport::hid::list_devices().await.unwrap(); - if devices.is_empty() { + let mut hid_devices = libwebauthn::transport::hid::list_devices().await.unwrap(); + if hid_devices.is_empty() { let state = UsbState::Waiting; - *self.usb_state.lock().await = state; + *self.usb_state.lock().await = state.clone(); return Ok(state); - } - if devices.is_empty() { - Ok(UsbState::Waiting) + } else if hid_devices.len() == 1 { + Ok(UsbState::Connected(hid_devices.swap_remove(0))) } else { - Ok(UsbState::Connected) + Ok(UsbState::SelectingDevice(hid_devices)) } } - UsbState::Connected => { - // TODO: I'm not sure how we want to handle multiple usb devices - // just take the first one found for now. - // TODO: store this device reference, perhaps in the enum itself + UsbState::SelectingDevice(hid_devices) => { + let (blinking_tx, mut blinking_rx) = + tokio::sync::mpsc::channel::>(hid_devices.len()); + let mut expected_answers = hid_devices.len(); + for mut device in hid_devices { + let tx = blinking_tx.clone(); + tokio().spawn(async move { + let (mut channel, _state_rx) = device.channel().await.unwrap(); + let res = channel + .blink_and_wait_for_user_presence(Duration::from_secs(300)) + .await; + drop(channel); + match res { + Ok(true) => { + let _ = tx.send(Some(device)).await; + } + Ok(false) | Err(_) => { + let _ = tx.send(None).await; + } + } + }); + } + let mut state = UsbState::Idle; + while let Some(msg) = blinking_rx.recv().await { + expected_answers -= 1; + match msg { + Some(device) => { + state = UsbState::Connected(device); + break; + } + None => { + if expected_answers == 0 { + break; + } else { + continue; + } + } + } + } + Ok(state) + } + UsbState::Connected(mut device) => { let handler = self.usb_uv_handler.clone(); let cred_request = self.cred_request.clone(); let signal_tx = self.usb_uv_handler.signal_tx.clone(); let pin_rx = self.usb_uv_handler.pin_rx.clone(); tokio().spawn(async move { - let mut devices = libwebauthn::transport::hid::list_devices().await.unwrap(); - let device = devices.first_mut().unwrap(); let (mut channel, state_rx) = device.channel().await.unwrap(); tokio().spawn(async move { handle_usb_updates(signal_tx, pin_rx, state_rx).await; @@ -252,7 +290,7 @@ impl CredentialService { UsbState::Completed => Ok(prev_usb_state), }?; - *self.usb_state.lock().await = next_usb_state; + *self.usb_state.lock().await = next_usb_state.clone(); Ok(next_usb_state) } @@ -263,7 +301,7 @@ impl CredentialService { } pub(crate) async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { - let current_state = *self.usb_state.lock().await; + let current_state = self.usb_state.lock().await.clone(); match current_state { UsbState::NeedsPin { attempts_left: Some(attempts_left), @@ -281,7 +319,7 @@ impl CredentialService { } } -#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub enum UsbState { /// Not polling for FIDO USB device. #[default] @@ -291,13 +329,17 @@ pub enum UsbState { Waiting, /// USB device connected, prompt user to tap - Connected, + Connected(HidDevice), /// The device needs the PIN to be entered. - NeedsPin { attempts_left: Option }, + NeedsPin { + attempts_left: Option, + }, /// The device needs on-device user verification. - NeedsUserVerification { attempts_left: Option }, + NeedsUserVerification { + attempts_left: Option, + }, /// The device needs evidence of user presence (e.g. touch) to release the credential. NeedsUserPresence, @@ -306,7 +348,11 @@ pub enum UsbState { Completed, // TODO: implement cancellation // This isn't actually sent from the server. - // UserCancelled, + //UserCancelled, + + // When we encounter multiple devices, we let all of them blink and continue + // with the one that was tapped. + SelectingDevice(Vec), } #[derive(Clone, Debug)] diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/gtk/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/gtk/mod.rs index 2294d72..8b2f911 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/gtk/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/gtk/mod.rs @@ -109,8 +109,9 @@ impl ViewModel { ViewUpdate::SetCredentials(credentials) => { view_model.update_credentials(&credentials) } - ViewUpdate::SelectDevice(device) => { - view_model.select_device(&device) + ViewUpdate::SelectingDevice => view_model.selecting_device(), + ViewUpdate::WaitingForDevice(device) => { + view_model.waiting_for_device(&device) } ViewUpdate::SelectCredential(cred_id) => { view_model.select_credential(cred_id) @@ -243,7 +244,7 @@ impl ViewModel { self.set_credentials(credential_list); } - fn select_device(&self, device: &Device) { + fn waiting_for_device(&self, device: &Device) { match device.transport { Transport::Usb => { self.set_prompt("Insert your security key."); @@ -257,6 +258,10 @@ impl ViewModel { self.set_selected_credential(""); } + fn selecting_device(&self) { + self.set_prompt("Multiple devices found. Please select with which to proceed."); + } + fn select_credential(&self, cred_id: String) { self.set_selected_credential(cred_id); } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/mod.rs index 5419df9..0059ace 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/view_model/mod.rs @@ -208,7 +208,7 @@ impl ViewModel { } self.tx_update - .send(ViewUpdate::SelectDevice(device.clone())) + .send(ViewUpdate::WaitingForDevice(device.clone())) .await .unwrap(); } @@ -282,7 +282,13 @@ impl ViewModel { self.credential_service.lock().await.complete_auth(); self.tx_update.send(ViewUpdate::Completed).await.unwrap(); } - _ => {} + UsbState::SelectingDevice => { + self.tx_update + .send(ViewUpdate::SelectingDevice) + .await + .unwrap(); + } + UsbState::NotListening | UsbState::Waiting | UsbState::UserCancelled => {} } } }; @@ -302,12 +308,13 @@ pub enum ViewUpdate { SetTitle(String), SetDevices(Vec), SetCredentials(Vec), - SelectDevice(Device), + WaitingForDevice(Device), SelectCredential(String), UsbNeedsPin { attempts_left: Option }, UsbNeedsUserVerification { attempts_left: Option }, UsbNeedsUserPresence, Completed, + SelectingDevice, } pub enum BackgroundEvent { @@ -465,14 +472,18 @@ pub enum UsbState { // This isn't actually sent from the server. UserCancelled, + + /// Multiple devices found + SelectingDevice, } impl From for UsbState { fn from(val: crate::credential_service::UsbState) -> Self { match val { crate::credential_service::UsbState::Idle => UsbState::NotListening, + crate::credential_service::UsbState::SelectingDevice(..) => UsbState::SelectingDevice, crate::credential_service::UsbState::Waiting => UsbState::Waiting, - crate::credential_service::UsbState::Connected => UsbState::Connected, + crate::credential_service::UsbState::Connected(..) => UsbState::Connected, crate::credential_service::UsbState::NeedsPin { attempts_left } => { UsbState::NeedsPin { attempts_left } }