From e372516be6ef779a267738e7d7b9f4ed86c638c7 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 07:12:57 -0500 Subject: [PATCH 01/13] ignore demo_client output --- demo_client/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 demo_client/.gitignore diff --git a/demo_client/.gitignore b/demo_client/.gitignore new file mode 100644 index 00000000..cb69d726 --- /dev/null +++ b/demo_client/.gitignore @@ -0,0 +1 @@ +user.json From 54593ff33334395c07b385e8e628cf8558a0504d Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 14:50:06 -0500 Subject: [PATCH 02/13] Remove unused Meson label --- xyz-iinuwa-credential-manager-portal-gtk/src/meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build index e44a361b..b9bb0a5c 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build @@ -9,7 +9,7 @@ global_conf.set_quoted('PROFILE', profile) global_conf.set_quoted('VERSION', version + version_suffix) global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) global_conf.set_quoted('LOCALEDIR', localedir) -config = configure_file( +configure_file( input: 'config.rs.in', output: 'config.rs', configuration: global_conf @@ -37,7 +37,7 @@ endif cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] -cargo_build = custom_target( +custom_target( 'cargo-build', build_by_default: true, build_always_stale: true, From 04dd5f0a924cbdce57d2cd87b4e3909a03a01c02 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 07:18:14 -0500 Subject: [PATCH 03/13] wip: move to Tokio --- .../Cargo.lock | 20 ++--------- .../Cargo.toml | 2 +- .../data/meson.build | 2 +- .../src/credential_service/hybrid.rs | 3 +- .../src/credential_service/mod.rs | 3 +- .../src/dbus.rs | 33 +++++++++---------- .../src/main.rs | 13 +++----- 7 files changed, 25 insertions(+), 51 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock index 6fb4c1a3..ada739b6 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock @@ -154,17 +154,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-global-executor" version = "2.4.1" @@ -3322,6 +3311,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -4014,15 +4004,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", "async-recursion", - "async-task", "async-trait", - "blocking", "enumflags2", "event-listener 5.4.0", "futures-core", @@ -4033,6 +4016,7 @@ dependencies = [ "serde", "serde_repr", "static_assertions", + "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml index ce66920f..5a36ee30 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml @@ -19,7 +19,7 @@ serde_json = "1.0.140" # serde_cbor = "0.11.1" tracing = "0.1.41" tracing-subscriber = "0.3" -zbus = "5.5.0" +zbus = { version = "5.5.0", default-features = false, features = ["blocking-api", "tokio"] } libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "528af8de3adfbc329b6bd6dca7bf48714e674f96" } async-trait = "0.1.88" tokio = { version = "1.45.0", features = ["rt-multi-thread"] } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/data/meson.build index 0cdf5d22..ead7b4c9 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/data/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/data/meson.build @@ -76,7 +76,7 @@ if glib_compile_schemas.found() endif if get_option('profile') == 'development' - gschema_target = custom_target('gschema', + custom_target('gschema', input : gschema_xml, output : 'gschema.compiled', command : [glib_compile_schemas, '--strict', meson.current_build_dir()], diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index 60f1569c..c616db55 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -2,8 +2,7 @@ use std::fmt::Debug; use std::task::Poll; use async_std::channel::Receiver; -use async_std::stream::Stream; -use futures_lite::FutureExt; +use futures_lite::{FutureExt, Stream}; use libwebauthn::fido::{AuthenticatorData, AuthenticatorDataFlags}; use libwebauthn::ops::webauthn::{Assertion, GetAssertionResponse}; use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transport}; 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 2bac255e..863002d0 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 @@ -7,8 +7,7 @@ use std::{ time::Duration, }; -use async_std::stream::Stream; -use futures_lite::{FutureExt, StreamExt}; +use futures_lite::{FutureExt, Stream, StreamExt}; use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index cfdee7a6..4a2bc0e5 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -3,8 +3,6 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use async_std::channel::{Receiver, Sender}; -use async_std::sync::Mutex as AsyncMutex; use base64::Engine; use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD}; use gettextrs::{gettext, LocaleCategory}; @@ -20,10 +18,12 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, }; -use zbus::zvariant::{DeserializeDict, SerializeDict, Type}; +use ring::digest; +use tokio::sync::{mpsc::{self, Receiver, Sender}, Mutex as AsyncMutex}; use zbus::{ connection::{self, Connection}, fdo, interface, Result, + zvariant::{DeserializeDict, SerializeDict, Type} }; use crate::application::ExampleApplication; @@ -36,10 +36,9 @@ use crate::view_model::{self, ViewEvent, ViewUpdate}; use crate::webauthn::{ self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, }; -use ring::digest; pub(crate) async fn start_service(service_name: &str, path: &str) -> Result { - let (gui_tx, gui_rx) = async_std::channel::bounded(1); + let (gui_tx, gui_rx) = mpsc::channel(1); let lock: Arc>)>>> = Arc::new(AsyncMutex::new(gui_tx)); start_gui_thread(gui_rx); @@ -50,11 +49,11 @@ pub(crate) async fn start_service(service_name: &str, path: &str) -> Result>)>) { +fn start_gui_thread(mut rx: Receiver<(CredentialRequest, Sender>)>) { thread::Builder::new() .name("gui".into()) .spawn(move || { - while let Ok((cred_request, response_tx)) = rx.recv_blocking() { + while let Some((cred_request, response_tx)) = rx.blocking_recv() { let (tx_update, rx_update) = async_std::channel::unbounded::(); let (tx_event, rx_event) = async_std::channel::unbounded::(); let data = Arc::new(Mutex::new(None)); @@ -86,13 +85,13 @@ fn start_gui_thread(rx: Receiver<(CredentialRequest, Sender, rx_update: Receiver) { +fn start_gtk_app(tx_event: async_std::channel::Sender, rx_update: async_std::channel::Receiver) { // Prepare i18n gettextrs::setlocale(LocaleCategory::LcAll, ""); gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); @@ -118,7 +117,7 @@ impl CredentialManager { &self, request: CreateCredentialRequest, ) -> fdo::Result { - if let Some(tx) = self.app_lock.try_lock() { + if let Ok(tx) = self.app_lock.try_lock() { if request.origin.is_none() { todo!("Implicit caller-origin binding not yet implemented.") }; @@ -138,9 +137,8 @@ impl CredentialManager { })?; let request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); - let (data_tx, data_rx) = async_std::channel::bounded(1); + let (data_tx, mut data_rx) = mpsc::channel(1); tx.send((request, data_tx)).await.unwrap(); - let data_rx = Arc::new(data_rx); if let Some(CredentialResponse::CreatePublicKeyCredentialResponse( cred_response, )) = data_rx.recv().await.unwrap() @@ -172,7 +170,7 @@ impl CredentialManager { &self, request: GetCredentialRequest, ) -> fdo::Result { - if let Some(tx) = self.app_lock.try_lock() { + if let Ok(tx) = self.app_lock.try_lock() { if request.origin.is_none() { todo!("Implicit caller-origin binding is not yet implemented."); } @@ -198,11 +196,10 @@ impl CredentialManager { })?; let request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); - let (data_tx, data_rx) = async_std::channel::bounded(1); + let (data_tx, mut data_rx) = mpsc::channel(1); tx.send((request, data_tx)).await.unwrap(); - let data_rx = Arc::new(data_rx); match data_rx.recv().await { - Ok(Some(CredentialResponse::GetPublicKeyCredentialResponse( + Some(Some(CredentialResponse::GetPublicKeyCredentialResponse( cred_response, ))) => { let public_key_response = @@ -212,10 +209,10 @@ impl CredentialManager { )?; Ok(public_key_response.into()) } - Ok(_) => Err(fdo::Error::Failed( + Some(_) => Err(fdo::Error::Failed( "Invalid credential response received from authenticator".to_string(), )), - Err(_) => Err(fdo::Error::Failed("User cancelled operation".to_string())), + None => Err(fdo::Error::Failed("User cancelled operation".to_string())), } } _ => Err(fdo::Error::Failed( diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index 50e2a7ed..8740a65e 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -14,9 +14,8 @@ mod window; use std::error::Error; -use async_std::task; - -fn main() { +#[tokio::main] +async fn main() { // Initialize logger tracing_subscriber::fmt::init(); rustls::crypto::ring::default_provider() @@ -25,20 +24,16 @@ fn main() { _ = tokio_runtime::get(); println!("Starting..."); - task::block_on(run()).unwrap(); + run().await.unwrap(); } async fn run() -> Result<(), Box> { let service_name = "xyz.iinuwa.credentials.CredentialManagerUi"; let path = "/xyz/iinuwa/credentials/CredentialManagerUi"; let _conn = dbus::start_service(service_name, path).await?; - // store::initialize(); - // let _conn = dbus::start_service(service_name, path, seed_key).await?; println!("Started"); loop { - // do something else, wait forever or timeout here: - // handling D-Bus messages is done in the background - + // wait forever, handle D-Bus in the background std::future::pending::<()>().await; } } From 4db382b88018097e8500b2231651c37ec83ae7ae Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 09:49:52 -0500 Subject: [PATCH 04/13] wip: Move CredentialService to trait --- .../src/credential_service/mod.rs | 45 ++++++++++++------- .../src/view_model/mod.rs | 12 ++--- 2 files changed, 37 insertions(+), 20 deletions(-) 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 863002d0..9c8e29be 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,10 +1,7 @@ pub mod hybrid; use std::{ - fmt::Debug, - sync::{Arc, Mutex}, - task::Poll, - time::Duration, + fmt::Debug, future::Future, pin::Pin, sync::{Arc, Mutex}, task::Poll, time::Duration }; use futures_lite::{FutureExt, Stream, StreamExt}; @@ -47,7 +44,7 @@ pub struct CredentialService { hybrid_handler: H, } -impl CredentialService { +impl CredentialService { pub fn new( cred_request: CredentialRequest, cred_response: Arc>>, @@ -76,12 +73,30 @@ impl CredentialService { hybrid_handler, } } +} + +pub trait CredentialServiceClient +where { + async fn get_available_public_key_devices(&self) -> Result, ()>; + + async fn poll_device_discovery_usb(&mut self) -> Result; + + async fn cancel_device_discovery_usb(&mut self) -> Result<(), String>; + + async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()>; - pub async fn get_available_public_key_devices(&self) -> Result, ()> { + fn complete_auth(&mut self); + + fn get_hybrid_credential(&self) -> Pin + Send>>; +} + +impl CredentialServiceClient for CredentialService +where ::Stream: Unpin + Send { + async fn get_available_public_key_devices(&self) -> Result, ()> { Ok(self.devices.to_owned()) } - pub(crate) async fn poll_device_discovery_usb(&mut self) -> Result { + async fn poll_device_discovery_usb(&mut self) -> Result { debug!("polling for USB status"); let prev_usb_state = self.usb_state.lock().await.clone(); let next_usb_state = match prev_usb_state { @@ -312,13 +327,13 @@ impl CredentialService { Ok(next_usb_state) } - pub(crate) async fn cancel_device_discovery_usb(&mut self) -> Result<(), String> { + async fn cancel_device_discovery_usb(&mut self) -> Result<(), String> { *self.usb_state.lock().await = UsbState::Idle; println!("frontend: Cancel USB request"); Ok(()) } - pub(crate) async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { + async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { let current_state = self.usb_state.lock().await.clone(); match current_state { UsbState::NeedsPin { @@ -331,17 +346,17 @@ impl CredentialService { } } - pub(crate) fn complete_auth(&mut self) { + fn complete_auth(&mut self) { // let mut data = self.output_data.lock().unwrap(); // data.replace((self.cred_response)); } - pub(crate) fn get_hybrid_credential(&self) -> HybridStateStream { + fn get_hybrid_credential(&self) -> Pin + Send>> { let stream = self.hybrid_handler.start(&self.cred_request); - HybridStateStream { + Box::pin(HybridStateStream { inner: stream, cred_response: self.cred_response.clone(), - } + }) } } @@ -569,7 +584,7 @@ mod test { use super::{ hybrid::{DummyHybridHandler, HybridStateInternal}, - AuthenticatorResponse, CredentialService, + AuthenticatorResponse, CredentialService, CredentialServiceClient }; #[test] @@ -587,7 +602,7 @@ mod test { ]); let cred_service = CredentialService::new(request, response, hybrid_handler); let mut stream = cred_service.get_hybrid_credential(); - async_std::task::block_on(async { while let Some(_) = stream.next().await {} }); + async_std::task::block_on(async move { while let Some(_) = stream.next().await {} }); assert!(cred_service.cred_response.lock().unwrap().is_some()); } 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 642ba197..419cd0b0 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 @@ -1,5 +1,6 @@ pub mod gtk; +use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; @@ -12,11 +13,12 @@ use tracing::info; // TODO: turn CredentialService into a trait so we don't have to do specify handler types manually. use crate::credential_service::hybrid::InternalHybridHandler; -use crate::credential_service::CredentialService; +use crate::credential_service::{CredentialService, CredentialServiceClient}; #[derive(Debug)] -pub(crate) struct ViewModel { - credential_service: Arc>>, +pub(crate) struct ViewModel +where C: CredentialServiceClient + Send { + credential_service: Arc>, tx_update: Sender, rx_event: Receiver, bg_update: Sender, @@ -40,10 +42,10 @@ pub(crate) struct ViewModel { hybrid_linked_state: HybridState, } -impl ViewModel { +impl ViewModel { pub(crate) fn new( operation: Operation, - credential_service: CredentialService, + credential_service: C, rx_event: Receiver, tx_update: Sender, ) -> Self { From 31213e12150b22d7aee718ab56e5b89bb9a0178b Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 14:46:44 -0500 Subject: [PATCH 05/13] wip: start work on moving usb to separate module --- .../src/credential_service/mod.rs | 386 +--------------- .../src/credential_service/usb.rs | 436 ++++++++++++++++++ 2 files changed, 439 insertions(+), 383 deletions(-) create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs 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 9c8e29be..6d1ad760 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,4 +1,5 @@ pub mod hybrid; +pub mod usb; use std::{ fmt::Debug, future::Future, pin::Pin, sync::{Arc, Mutex}, task::Poll, time::Duration @@ -14,7 +15,6 @@ use libwebauthn::{ }; use async_std::{ - channel::TryRecvError, sync::{Arc as AsyncArc, Mutex as AsyncMutex}, }; use tracing::{debug, warn}; @@ -29,6 +29,8 @@ use crate::{ }; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; +use usb::UsbUvHandler; +pub use usb::UsbState; #[derive(Debug)] pub struct CredentialService { @@ -97,253 +99,6 @@ where ::Stream: Unpin + Send { } async fn poll_device_discovery_usb(&mut self) -> Result { - debug!("polling for USB status"); - let prev_usb_state = self.usb_state.lock().await.clone(); - let next_usb_state = match prev_usb_state { - UsbState::Idle | UsbState::Waiting => { - 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.clone(); - return Ok(state); - } else if hid_devices.len() == 1 { - Ok(UsbState::Connected(hid_devices.swap_remove(0))) - } else { - Ok(UsbState::SelectingDevice(hid_devices)) - } - } - 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_runtime::get().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_runtime::get().spawn(async move { - let (mut channel, state_rx) = device.channel().await.unwrap(); - tokio_runtime::get().spawn(async move { - handle_usb_updates(signal_tx, pin_rx, state_rx).await; - debug!("Reached end of USB update task"); - }); - match cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => { - loop { - match channel.webauthn_make_credential(&make_cred_request).await { - Ok(response) => { - handler - .notify_ceremony_completed( - AuthenticatorResponse::CredentialCreated(response), - ) - .await; - break; - } - Err(WebAuthnError::Ctap(ctap_error)) - if ctap_error.is_retryable_user_error() => - { - warn!("Retrying WebAuthn make credential operation"); - continue; - } - Err(err) => { - handler.notify_ceremony_failed(err.to_string()).await; - break; - } - }; - } - } - CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => { - loop { - match channel.webauthn_get_assertion(&get_cred_request).await { - Ok(response) => { - handler - .notify_ceremony_completed( - AuthenticatorResponse::CredentialsAsserted( - response, - ), - ) - .await; - break; - } - Err(WebAuthnError::Ctap(ctap_error)) - if ctap_error.is_retryable_user_error() => - { - warn!("Retrying WebAuthn get credential operation"); - continue; - } - Err(err) => { - handler.notify_ceremony_failed(err.to_string()).await; - break; - } - }; - } - } - }; - }); - match self.usb_uv_handler.wait_for_notification().await { - Ok(UsbUvMessage::NeedsPin { attempts_left }) => { - Ok(UsbState::NeedsPin { attempts_left }) - } - Ok(UsbUvMessage::NeedsUserVerification { attempts_left }) => { - Ok(UsbState::NeedsUserVerification { attempts_left }) - } - Ok(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), - Ok(UsbUvMessage::ReceivedCredential(response)) => { - match response { - AuthenticatorResponse::CredentialCreated(r) => { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace( - CredentialResponse::CreatePublicKeyCredentialResponse( - MakeCredentialResponseInternal::new( - r, - vec![String::from("usb")], - String::from("cross-platform"), - ), - ), - ); - Ok(UsbState::Completed) - } - AuthenticatorResponse::CredentialsAsserted(r) => { - // at least one credential is returned from the authenticator - assert!(!r.assertions.is_empty()); - if r.assertions.len() == 1 { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace( - CredentialResponse::GetPublicKeyCredentialResponse( - GetAssertionResponseInternal::new( - r.assertions[0].clone(), - String::from("cross-platform"), - ), - ), - ); - Ok(UsbState::Completed) - } else { - todo!("need to support selection from multiple credentials"); - } - } - } - } - Err(err) => Err(err), - } - } - UsbState::NeedsPin { - attempts_left: Some(attempts_left), - } if attempts_left <= 1 => Err("No more USB attempts left".to_string()), - UsbState::NeedsUserVerification { - attempts_left: Some(attempts_left), - } if attempts_left <= 1 => { - Err("No more on-device user device attempts left".to_string()) - } - UsbState::NeedsPin { .. } - | UsbState::NeedsUserVerification { .. } - | UsbState::NeedsUserPresence => { - match self.usb_uv_handler.check_notification().await? { - Some(UsbUvMessage::NeedsPin { attempts_left }) => { - Ok(UsbState::NeedsPin { attempts_left }) - } - Some(UsbUvMessage::NeedsUserVerification { attempts_left }) => { - Ok(UsbState::NeedsUserVerification { attempts_left }) - } - Some(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), - Some(UsbUvMessage::ReceivedCredential(response)) => { - match response { - AuthenticatorResponse::CredentialCreated(r) => { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace( - CredentialResponse::CreatePublicKeyCredentialResponse( - MakeCredentialResponseInternal::new( - r, - vec![String::from("usb")], - String::from("cross-platform"), - ), - ), - ); - Ok(UsbState::Completed) - } - AuthenticatorResponse::CredentialsAsserted(r) => { - // at least one credential is returned from the authenticator - assert!(!r.assertions.is_empty()); - if r.assertions.len() == 1 { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace( - CredentialResponse::GetPublicKeyCredentialResponse( - GetAssertionResponseInternal::new( - r.assertions[0].clone(), - String::from("cross-platform"), - ), - ), - ); - Ok(UsbState::Completed) - } else { - todo!("need to support selection from multiple credentials"); - } - } - } - } - None => Ok(prev_usb_state), - } - } - UsbState::Completed => Ok(prev_usb_state), - }?; - - *self.usb_state.lock().await = next_usb_state.clone(); - Ok(next_usb_state) - } - - async fn cancel_device_discovery_usb(&mut self) -> Result<(), String> { - *self.usb_state.lock().await = UsbState::Idle; - println!("frontend: Cancel USB request"); - Ok(()) - } - - async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { - let current_state = self.usb_state.lock().await.clone(); - match current_state { - UsbState::NeedsPin { - attempts_left: Some(attempts_left), - } if attempts_left > 1 => { - self.usb_uv_handler.send_pin(pin).await; - Ok(()) - } - _ => Err(()), - } } fn complete_auth(&mut self) { @@ -418,141 +173,6 @@ where } } -#[derive(Clone, Debug, Default)] -pub enum UsbState { - /// Not polling for FIDO USB device. - #[default] - Idle, - - /// Awaiting FIDO USB device to be plugged in. - Waiting, - - /// USB device connected, prompt user to tap - Connected(HidDevice), - - /// The device needs the PIN to be entered. - NeedsPin { - attempts_left: Option, - }, - - /// The device needs on-device user verification. - NeedsUserVerification { - attempts_left: Option, - }, - - /// The device needs evidence of user presence (e.g. touch) to release the credential. - NeedsUserPresence, - - /// USB tapped, received credential - Completed, - // TODO: implement cancellation - // This isn't actually sent from the server. - //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)] -pub struct UsbUvHandler { - signal_tx: async_std::channel::Sender>, - signal_rx: async_std::channel::Receiver>, - pin_tx: async_std::channel::Sender, - pin_rx: async_std::channel::Receiver, -} - -impl UsbUvHandler { - fn new() -> Self { - let (signal_tx, signal_rx) = async_std::channel::unbounded(); - let (pin_tx, pin_rx) = async_std::channel::unbounded(); - UsbUvHandler { - signal_tx, - signal_rx, - pin_tx, - pin_rx, - } - } - - async fn notify_ceremony_completed(&self, response: AuthenticatorResponse) { - self.signal_tx - .send(Ok(UsbUvMessage::ReceivedCredential(response))) - .await - .unwrap(); - } - - async fn notify_ceremony_failed(&self, err: String) { - self.signal_tx.send(Err(err)).await.unwrap(); - } - - async fn send_pin(&self, pin: &str) { - self.pin_tx.send(pin.to_owned()).await.unwrap(); - } - - async fn wait_for_notification(&self) -> Result { - match self.signal_rx.recv().await { - Ok(msg) => msg, - Err(err) => Err(err.to_string()), - } - } - - async fn check_notification(&self) -> Result, String> { - match self.signal_rx.try_recv() { - Ok(msg) => Ok(Some(msg?)), - Err(TryRecvError::Empty) => Ok(None), - Err(TryRecvError::Closed) => Err("USB UV handler channel closed".to_string()), - } - } -} - -async fn handle_usb_updates( - signal_tx: async_std::channel::Sender>, - pin_rx: async_std::channel::Receiver, - mut state_rx: tokio::sync::mpsc::Receiver, -) { - while let Some(msg) = state_rx.recv().await { - match msg { - UxUpdate::UvRetry { attempts_left } => { - signal_tx - .send(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })) - .await - .unwrap(); - } - UxUpdate::PinRequired(pin_update) => { - if pin_update.attempts_left.is_some_and(|num| num <= 1) { - // TODO: cancel authenticator operation - signal_tx.send(Err("No more PIN attempts allowed. Select a different authenticator or try again later.".to_string())).await.unwrap(); - continue; - } - signal_tx - .send(Ok(UsbUvMessage::NeedsPin { - attempts_left: pin_update.attempts_left, - })) - .await - .unwrap(); - if let Ok(pin) = pin_rx.recv().await { - pin_update.send_pin(&pin).unwrap(); - } else { - debug!("PIN channel closed."); - } - } - UxUpdate::PresenceRequired => { - signal_tx - .send(Ok(UsbUvMessage::NeedsUserPresence)) - .await - .unwrap(); - } - } - } - debug!("USB update channel closed."); -} - -enum UsbUvMessage { - NeedsPin { attempts_left: Option }, - NeedsUserVerification { attempts_left: Option }, - NeedsUserPresence, - ReceivedCredential(AuthenticatorResponse), -} #[derive(Debug, Clone)] enum AuthenticatorResponse { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs new file mode 100644 index 00000000..780cb1ea --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs @@ -0,0 +1,436 @@ +use std::time::Duration; + +use async_std::channel::TryRecvError; +use futures_lite::Stream; +use libwebauthn::{transport::{hid::HidDevice, Device}, webauthn::{WebAuthn, Error as WebAuthnError}, UxUpdate}; +use tracing::debug; + +use crate::{dbus::{CredentialRequest, CredentialResponse, GetAssertionResponseInternal, MakeCredentialResponseInternal}, tokio_runtime}; + +use super::AuthenticatorResponse; + +pub(crate) trait UsbHandler { + type Stream: Stream; + fn start(&self, request: &CredentialRequest) -> Self::Stream; +} + +pub struct LocalUsbHandler { + usb_state: AsyncArc>, +} +impl LocalUsbHandler { + +} + +impl UsbHandler for LocalUsbHandler { + type Stream = LocalUsbStateStream; + + fn start(&self, request: &CredentialRequest) -> Self::Stream { + todo!() + } + +} + +struct LocalUsbStateStream { + usb_state: UsbState, + usb_uv_handler: UsbUvHandler, +} + +impl Stream for LocalUsbStateStream { + type Item = UsbState; + + fn poll_next(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll> { + debug!("polling for USB status"); + let prev_usb_state = self.usb_state; + let next_usb_state = match prev_usb_state { + UsbState::Idle | UsbState::Waiting => { + let mut hid_devices = libwebauthn::transport::hid::list_devices().await.unwrap(); + if hid_devices.is_empty() { + let state = UsbState::Waiting; + self.usb_state = state.clone(); + Ok(state) + } else if hid_devices.len() == 1 { + Ok(UsbState::Connected(hid_devices.swap_remove(0))) + } else { + Ok(UsbState::SelectingDevice(hid_devices)) + } + } + 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_runtime::get().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_runtime::get().spawn(async move { + let (mut channel, state_rx) = device.channel().await.unwrap(); + tokio_runtime::get().spawn(async move { + handle_usb_updates(signal_tx, pin_rx, state_rx).await; + debug!("Reached end of USB update task"); + }); + match cred_request { + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => { + loop { + match channel.webauthn_make_credential(&make_cred_request).await { + Ok(response) => { + handler + .notify_ceremony_completed( + AuthenticatorResponse::CredentialCreated(response), + ) + .await; + break; + } + Err(WebAuthnError::Ctap(ctap_error)) + if ctap_error.is_retryable_user_error() => + { + warn!("Retrying WebAuthn make credential operation"); + continue; + } + Err(err) => { + handler.notify_ceremony_failed(err.to_string()).await; + break; + } + }; + } + } + CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => { + loop { + match channel.webauthn_get_assertion(&get_cred_request).await { + Ok(response) => { + handler + .notify_ceremony_completed( + AuthenticatorResponse::CredentialsAsserted( + response, + ), + ) + .await; + break; + } + Err(WebAuthnError::Ctap(ctap_error)) + if ctap_error.is_retryable_user_error() => + { + warn!("Retrying WebAuthn get credential operation"); + continue; + } + Err(err) => { + handler.notify_ceremony_failed(err.to_string()).await; + break; + } + }; + } + } + }; + }); + match self.usb_uv_handler.wait_for_notification().await { + Ok(UsbUvMessage::NeedsPin { attempts_left }) => { + Ok(UsbState::NeedsPin { attempts_left }) + } + Ok(UsbUvMessage::NeedsUserVerification { attempts_left }) => { + Ok(UsbState::NeedsUserVerification { attempts_left }) + } + Ok(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), + Ok(UsbUvMessage::ReceivedCredential(response)) => { + match response { + AuthenticatorResponse::CredentialCreated(r) => { + let mut cred_response = self.cred_response.lock().unwrap(); + cred_response.replace( + CredentialResponse::CreatePublicKeyCredentialResponse( + MakeCredentialResponseInternal::new( + r, + vec![String::from("usb")], + String::from("cross-platform"), + ), + ), + ); + Ok(UsbState::Completed) + } + AuthenticatorResponse::CredentialsAsserted(r) => { + // at least one credential is returned from the authenticator + assert!(!r.assertions.is_empty()); + if r.assertions.len() == 1 { + let mut cred_response = self.cred_response.lock().unwrap(); + cred_response.replace( + CredentialResponse::GetPublicKeyCredentialResponse( + GetAssertionResponseInternal::new( + r.assertions[0].clone(), + String::from("cross-platform"), + ), + ), + ); + Ok(UsbState::Completed) + } else { + todo!("need to support selection from multiple credentials"); + } + } + } + } + Err(err) => Err(err), + } + } + UsbState::NeedsPin { + attempts_left: Some(attempts_left), + } if attempts_left <= 1 => Err("No more USB attempts left".to_string()), + UsbState::NeedsUserVerification { + attempts_left: Some(attempts_left), + } if attempts_left <= 1 => { + Err("No more on-device user device attempts left".to_string()) + } + UsbState::NeedsPin { .. } + | UsbState::NeedsUserVerification { .. } + | UsbState::NeedsUserPresence => { + match self.usb_uv_handler.check_notification().await? { + Some(UsbUvMessage::NeedsPin { attempts_left }) => { + Ok(UsbState::NeedsPin { attempts_left }) + } + Some(UsbUvMessage::NeedsUserVerification { attempts_left }) => { + Ok(UsbState::NeedsUserVerification { attempts_left }) + } + Some(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), + Some(UsbUvMessage::ReceivedCredential(response)) => { + match response { + AuthenticatorResponse::CredentialCreated(r) => { + self.usb_state = UsbState::Completed; + Ok(CredentialResponse::CreatePublicKeyCredentialResponse( + MakeCredentialResponseInternal::new( + r, + vec![String::from("usb")], + String::from("cross-platform"), + ), + )) + } + AuthenticatorResponse::CredentialsAsserted(r) => { + // at least one credential is returned from the authenticator + assert!(!r.assertions.is_empty()); + if r.assertions.len() == 1 { + Ok(CredentialResponse::GetPublicKeyCredentialResponse( + GetAssertionResponseInternal::new( + r.assertions[0].clone(), + String::from("cross-platform"), + ) + )) + } else { + todo!("need to support selection from multiple credentials"); + } + } + } + } + None => Ok(prev_usb_state), + } + } + UsbState::Completed => Ok(prev_usb_state), + }?; + + self.usb_state = next_usb_state.clone(); + Ok(next_usb_state) + } + + /* + async fn cancel_device_discovery_usb(&mut self) -> Result<(), String> { + *self.usb_state.lock().await = UsbState::Idle; + println!("frontend: Cancel USB request"); + Ok(()) + } + */ + + /* + async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { + let current_state = self.usb_state.lock().await.clone(); + match current_state { + UsbState::NeedsPin { + attempts_left: Some(attempts_left), + } if attempts_left > 1 => { + self.usb_uv_handler.send_pin(pin).await; + Ok(()) + } + _ => Err(()), + } + } + */ +} + +#[derive(Clone, Debug, Default)] +pub enum UsbState { + /// Not polling for FIDO USB device. + #[default] + Idle, + + /// Awaiting FIDO USB device to be plugged in. + Waiting, + + /// USB device connected, prompt user to tap + Connected(HidDevice), + + /// The device needs the PIN to be entered. + NeedsPin { + attempts_left: Option, + }, + + /// The device needs on-device user verification. + NeedsUserVerification { + attempts_left: Option, + }, + + /// The device needs evidence of user presence (e.g. touch) to release the credential. + NeedsUserPresence, + + /// USB tapped, received credential + Completed, + // TODO: implement cancellation + // This isn't actually sent from the server. + //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)] +pub struct UsbUvHandler { + signal_tx: async_std::channel::Sender>, + signal_rx: async_std::channel::Receiver>, + pin_tx: async_std::channel::Sender, + pin_rx: async_std::channel::Receiver, +} + +impl UsbUvHandler { + pub fn new() -> Self { + let (signal_tx, signal_rx) = async_std::channel::unbounded(); + let (pin_tx, pin_rx) = async_std::channel::unbounded(); + UsbUvHandler { + signal_tx, + signal_rx, + pin_tx, + pin_rx, + } + } +} +trait UsbUvHandlerTrait { + async fn notify_ceremony_completed(&self, response: AuthenticatorResponse); + + async fn notify_ceremony_failed(&self, err: String); + + async fn send_pin(&self, pin: &str); + + async fn wait_for_notification(&self) -> Result; + + async fn check_notification(&self) -> Result, String>; +} + +impl UsbUvHandlerTrait for UsbUvHandler { + async fn notify_ceremony_completed(&self, response: AuthenticatorResponse) { + self.signal_tx + .send(Ok(UsbUvMessage::ReceivedCredential(response))) + .await + .unwrap(); + } + + async fn notify_ceremony_failed(&self, err: String) { + self.signal_tx.send(Err(err)).await.unwrap(); + } + + async fn send_pin(&self, pin: &str) { + self.pin_tx.send(pin.to_owned()).await.unwrap(); + } + + async fn wait_for_notification(&self) -> Result { + match self.signal_rx.recv().await { + Ok(msg) => msg, + Err(err) => Err(err.to_string()), + } + } + + async fn check_notification(&self) -> Result, String> { + match self.signal_rx.try_recv() { + Ok(msg) => Ok(Some(msg?)), + Err(TryRecvError::Empty) => Ok(None), + Err(TryRecvError::Closed) => Err("USB UV handler channel closed".to_string()), + } + } +} + +async fn handle_usb_updates( + signal_tx: async_std::channel::Sender>, + pin_rx: async_std::channel::Receiver, + mut state_rx: tokio::sync::mpsc::Receiver, +) { + while let Some(msg) = state_rx.recv().await { + match msg { + UxUpdate::UvRetry { attempts_left } => { + signal_tx + .send(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })) + .await + .unwrap(); + } + UxUpdate::PinRequired(pin_update) => { + if pin_update.attempts_left.is_some_and(|num| num <= 1) { + // TODO: cancel authenticator operation + signal_tx.send(Err("No more PIN attempts allowed. Select a different authenticator or try again later.".to_string())).await.unwrap(); + continue; + } + signal_tx + .send(Ok(UsbUvMessage::NeedsPin { + attempts_left: pin_update.attempts_left, + })) + .await + .unwrap(); + if let Ok(pin) = pin_rx.recv().await { + pin_update.send_pin(&pin).unwrap(); + } else { + debug!("PIN channel closed."); + } + } + UxUpdate::PresenceRequired => { + signal_tx + .send(Ok(UsbUvMessage::NeedsUserPresence)) + .await + .unwrap(); + } + } + } + debug!("USB update channel closed."); +} + +enum UsbUvMessage { + NeedsPin { attempts_left: Option }, + NeedsUserVerification { attempts_left: Option }, + NeedsUserPresence, + ReceivedCredential(AuthenticatorResponse), +} From 90c2dd3e9a6d5645578882068aacf22d18961f77 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 4 Jun 2025 09:02:51 -0500 Subject: [PATCH 06/13] Add icons --- .../data/icons/check-round-outline-symbolic.svg | 2 ++ .../data/icons/dialpad-symbolic.svg | 2 ++ .../data/icons/fingerprint-symbolic.svg | 2 ++ .../data/icons/symbolic-link-symbolic.svg | 2 ++ 4 files changed, 8 insertions(+) create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/data/icons/check-round-outline-symbolic.svg create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/data/icons/dialpad-symbolic.svg create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/data/icons/fingerprint-symbolic.svg create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/data/icons/symbolic-link-symbolic.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/check-round-outline-symbolic.svg b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/check-round-outline-symbolic.svg new file mode 100644 index 00000000..8d219c30 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/check-round-outline-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/dialpad-symbolic.svg b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/dialpad-symbolic.svg new file mode 100644 index 00000000..5b16830f --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/dialpad-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/fingerprint-symbolic.svg b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/fingerprint-symbolic.svg new file mode 100644 index 00000000..ba704cfa --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/fingerprint-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/symbolic-link-symbolic.svg b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/symbolic-link-symbolic.svg new file mode 100644 index 00000000..05a512c9 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/data/icons/symbolic-link-symbolic.svg @@ -0,0 +1,2 @@ + + From b8610d60adafa749d597f0607db5963a61334d16 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 14:52:37 -0500 Subject: [PATCH 07/13] Make stream live long enough --- .../src/credential_service/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 6d1ad760..eda56d71 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 @@ -92,9 +92,13 @@ where { fn get_hybrid_credential(&self) -> Pin + Send>>; } -impl CredentialServiceClient for CredentialService -where ::Stream: Unpin + Send { - async fn get_available_public_key_devices(&self) -> Result, ()> { +impl CredentialServiceClient + for CredentialService +where + ::Stream: Unpin + Send + 'static, + ::Stream: Unpin + Send + 'static, +{ + async fn get_available_public_key_devices(&self) -> Result, ()> { Ok(self.devices.to_owned()) } @@ -106,7 +110,7 @@ where ::Stream: Unpin + Send { // data.replace((self.cred_response)); } - fn get_hybrid_credential(&self) -> Pin + Send>> { + fn get_hybrid_credential(&self) -> Pin + Send + 'static>> { let stream = self.hybrid_handler.start(&self.cred_request); Box::pin(HybridStateStream { inner: stream, From 849e63d7d8d05c414fa4855bcf87ee1c47cdd19d Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 3 Jun 2025 14:52:37 -0500 Subject: [PATCH 08/13] wip: use stream for USB state --- .../src/credential_service/hybrid.rs | 14 +- .../src/credential_service/mod.rs | 169 +++-- .../src/credential_service/usb.rs | 635 +++++++++--------- .../src/dbus.rs | 17 +- .../src/main.rs | 2 - .../src/tokio_runtime.rs | 9 - .../src/view_model/mod.rs | 192 ++---- 7 files changed, 508 insertions(+), 530 deletions(-) delete mode 100644 xyz-iinuwa-credential-manager-portal-gtk/src/tokio_runtime.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index c616db55..cda29131 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -10,7 +10,7 @@ use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOpe use libwebauthn::transport::Device; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; -use crate::{dbus::CredentialRequest, tokio_runtime}; +use crate::dbus::CredentialRequest; use super::AuthenticatorResponse; @@ -33,7 +33,7 @@ impl HybridHandler for InternalHybridHandler { fn start(&self, request: &CredentialRequest) -> Self::Stream { let request = request.clone(); let (tx, rx) = async_std::channel::unbounded(); - async_std::task::spawn(async move { + tokio::spawn(async move { let hint = match request { CredentialRequest::CreatePublicKeyCredentialRequest(_) => { QrCodeOperationHint::MakeCredential @@ -48,8 +48,14 @@ impl HybridHandler for InternalHybridHandler { tracing::error!("Failed to send caBLE update: {:?}", err); return; }; - tokio_runtime::get().spawn(async move { - let (mut channel, _) = device.channel().await.unwrap(); + tokio::spawn(async move { + let mut channel = match device.channel().await { + Ok((channel, _)) => channel, + Err(e) => { + tracing::error!("Failed to open hybrid channel: {:?}", e); + panic!(); + } + }; let response: AuthenticatorResponse = loop { match &request { CredentialRequest::CreatePublicKeyCredentialRequest(make_request) => { 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 eda56d71..3a511d9e 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 @@ -2,55 +2,48 @@ pub mod hybrid; pub mod usb; use std::{ - fmt::Debug, future::Future, pin::Pin, sync::{Arc, Mutex}, task::Poll, time::Duration + fmt::Debug, + pin::Pin, + sync::{Arc, Mutex}, + task::Poll, }; use futures_lite::{FutureExt, Stream, StreamExt}; use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, - transport::{hid::HidDevice, Device as _}, - webauthn::{Error as WebAuthnError, WebAuthn}, - UxUpdate, }; -use async_std::{ - sync::{Arc as AsyncArc, Mutex as AsyncMutex}, -}; -use tracing::{debug, warn}; - use crate::{ dbus::{ CredentialRequest, CredentialResponse, GetAssertionResponseInternal, MakeCredentialResponseInternal, }, - tokio_runtime, view_model::{Device, Transport}, }; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; -use usb::UsbUvHandler; pub use usb::UsbState; +use usb::{UsbHandler, UsbStateInternal}; #[derive(Debug)] -pub struct CredentialService { +pub struct CredentialService { devices: Vec, - usb_state: AsyncArc>, - usb_uv_handler: UsbUvHandler, - cred_request: CredentialRequest, // Place to store data to be returned to the caller cred_response: Arc>>, hybrid_handler: H, + usb_handler: U, } -impl CredentialService { +impl CredentialService { pub fn new( cred_request: CredentialRequest, cred_response: Arc>>, hybrid_handler: H, + usb_handler: U, ) -> Self { let devices = vec![ Device { @@ -62,34 +55,25 @@ impl CredentialService { transport: Transport::HybridQr, }, ]; - let usb_state = AsyncArc::new(AsyncMutex::new(UsbState::Idle)); Self { devices, - usb_state: usb_state.clone(), - usb_uv_handler: UsbUvHandler::new(), - cred_request, cred_response, hybrid_handler, + usb_handler, } } } -pub trait CredentialServiceClient -where { - async fn get_available_public_key_devices(&self) -> Result, ()>; - - async fn poll_device_discovery_usb(&mut self) -> Result; - - async fn cancel_device_discovery_usb(&mut self) -> Result<(), String>; - - async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()>; +pub trait CredentialServiceClient { + async fn get_available_public_key_devices(&self) -> Result, ()>; - fn complete_auth(&mut self); + fn get_hybrid_credential(&self) -> Pin + Send>>; + fn get_usb_credential(&self) -> Pin + Send>>; - fn get_hybrid_credential(&self) -> Pin + Send>>; + fn complete_auth(&mut self); } impl CredentialServiceClient @@ -102,21 +86,27 @@ where Ok(self.devices.to_owned()) } - async fn poll_device_discovery_usb(&mut self) -> Result { - } - - fn complete_auth(&mut self) { - // let mut data = self.output_data.lock().unwrap(); - // data.replace((self.cred_response)); - } - fn get_hybrid_credential(&self) -> Pin + Send + 'static>> { let stream = self.hybrid_handler.start(&self.cred_request); + let cred_response = self.cred_response.clone(); Box::pin(HybridStateStream { + inner: stream, + cred_response, + }) + } + + fn get_usb_credential(&self) -> Pin + Send + 'static>> { + let stream = self.usb_handler.start(&self.cred_request); + Box::pin(UsbStateStream { inner: stream, cred_response: self.cred_response.clone(), }) } + + fn complete_auth(&mut self) { + // let mut data = self.output_data.lock().unwrap(); + // data.replace((self.cred_response)); + } } pub struct HybridStateStream { @@ -131,42 +121,16 @@ where type Item = HybridState; fn poll_next( - self: std::pin::Pin<&mut Self>, + self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { + ) -> Poll> { let cred_response = &self.cred_response.clone(); match Box::pin(Box::pin(self).as_mut().inner.next()).poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(Some(state)) => { if let HybridStateInternal::Completed(hybrid_response) = &state { - let response = match hybrid_response { - AuthenticatorResponse::CredentialCreated(make_response) => { - CredentialResponse::CreatePublicKeyCredentialResponse( - MakeCredentialResponseInternal::new( - make_response.clone(), - vec![String::from("hybrid")], - String::from("cross-platform"), - ), - ) - } - - AuthenticatorResponse::CredentialsAsserted(GetAssertionResponse { - assertions, - }) if assertions.len() == 1 => { - CredentialResponse::GetPublicKeyCredentialResponse( - GetAssertionResponseInternal::new( - assertions[0].clone(), - String::from("cross-platform"), - ), - ) - } - AuthenticatorResponse::CredentialsAsserted(GetAssertionResponse { - assertions, - }) => { - assert!(!assertions.is_empty()); - todo!("need to support selection from multiple credentials"); - } - }; + let response = + hybrid_response.into_cred_response(&["hybrid"], "cross-platform"); let mut cred_response = cred_response.lock().unwrap(); cred_response.replace(response); } @@ -177,12 +141,69 @@ where } } +struct UsbStateStream { + inner: H, + cred_response: Arc>>, +} + +impl Stream for UsbStateStream +where + H: Stream + Unpin + Sized, +{ + type Item = UsbState; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let cred_response = &self.cred_response.clone(); + match Box::pin(Box::pin(self).as_mut().inner.next()).poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Some(state)) => { + if let UsbStateInternal::Completed(response) = &state { + let response = response.into_cred_response(&["usb"], "cross-platform"); + let mut cred_response = cred_response.lock().unwrap(); + cred_response.replace(response); + } + Poll::Ready(Some(state.into())) + } + Poll::Ready(None) => Poll::Ready(None), + } + } +} #[derive(Debug, Clone)] enum AuthenticatorResponse { CredentialCreated(MakeCredentialResponse), CredentialsAsserted(GetAssertionResponse), } +impl AuthenticatorResponse { + fn into_cred_response(&self, transports: &[&str], modality: &str) -> CredentialResponse { + match self { + AuthenticatorResponse::CredentialCreated(make_response) => { + CredentialResponse::CreatePublicKeyCredentialResponse( + MakeCredentialResponseInternal::new( + make_response.clone(), + transports.iter().map(|s| s.to_string()).collect(), + modality.to_string(), + ), + ) + } + + AuthenticatorResponse::CredentialsAsserted(GetAssertionResponse { assertions }) + if assertions.len() == 1 => + { + CredentialResponse::GetPublicKeyCredentialResponse( + GetAssertionResponseInternal::new(assertions[0].clone(), modality.to_string()), + ) + } + AuthenticatorResponse::CredentialsAsserted(GetAssertionResponse { assertions }) => { + assert!(!assertions.is_empty()); + todo!("need to support selection from multiple credentials"); + } + } + } +} impl From for AuthenticatorResponse { fn from(value: MakeCredentialResponse) -> Self { @@ -202,13 +223,14 @@ mod test { use async_std::stream::StreamExt; - use crate::dbus::{ - CreateCredentialRequest, CreatePublicKeyCredentialRequest, CredentialRequest, + use crate::{ + credential_service::usb::LocalUsbHandler, + dbus::{CreateCredentialRequest, CreatePublicKeyCredentialRequest, CredentialRequest}, }; use super::{ hybrid::{DummyHybridHandler, HybridStateInternal}, - AuthenticatorResponse, CredentialService, CredentialServiceClient + AuthenticatorResponse, CredentialService, CredentialServiceClient, }; #[test] @@ -224,7 +246,8 @@ mod test { HybridStateInternal::Connecting, HybridStateInternal::Completed(authenticator_response), ]); - let cred_service = CredentialService::new(request, response, hybrid_handler); + let usb_handler = LocalUsbHandler {}; + let cred_service = CredentialService::new(request, response, hybrid_handler, usb_handler); let mut stream = cred_service.get_hybrid_credential(); async_std::task::block_on(async move { while let Some(_) = stream.next().await {} }); assert!(cred_service.cred_response.lock().unwrap().is_some()); diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs index 780cb1ea..016ef5ce 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs @@ -1,268 +1,172 @@ -use std::time::Duration; +use std::{ops::DerefMut, sync::Arc, time::Duration}; -use async_std::channel::TryRecvError; use futures_lite::Stream; -use libwebauthn::{transport::{hid::HidDevice, Device}, webauthn::{WebAuthn, Error as WebAuthnError}, UxUpdate}; -use tracing::debug; - -use crate::{dbus::{CredentialRequest, CredentialResponse, GetAssertionResponseInternal, MakeCredentialResponseInternal}, tokio_runtime}; +use libwebauthn::{ + transport::{hid::HidDevice, Device}, + webauthn::{Error as WebAuthnError, WebAuthn}, + UxUpdate, +}; +use tokio::sync::{ + mpsc::{self, error::TryRecvError, Receiver, Sender, WeakSender}, + oneshot, +}; +use tracing::{debug, warn}; + +use crate::dbus::CredentialRequest; use super::AuthenticatorResponse; pub(crate) trait UsbHandler { - type Stream: Stream; + type Stream: Stream; fn start(&self, request: &CredentialRequest) -> Self::Stream; } -pub struct LocalUsbHandler { - usb_state: AsyncArc>, -} -impl LocalUsbHandler { - -} - -impl UsbHandler for LocalUsbHandler { - type Stream = LocalUsbStateStream; - - fn start(&self, request: &CredentialRequest) -> Self::Stream { - todo!() - } - -} - -struct LocalUsbStateStream { - usb_state: UsbState, - usb_uv_handler: UsbUvHandler, -} - -impl Stream for LocalUsbStateStream { - type Item = UsbState; +#[derive(Debug)] +pub struct LocalUsbHandler {} - fn poll_next(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll> { +impl LocalUsbHandler { + async fn process( + tx: Sender, + cred_request: CredentialRequest, + ) -> Result<(), String> { + let mut state = UsbStateInternal::Idle; + let (signal_tx, mut signal_rx) = mpsc::channel(256); debug!("polling for USB status"); - let prev_usb_state = self.usb_state; - let next_usb_state = match prev_usb_state { - UsbState::Idle | UsbState::Waiting => { - let mut hid_devices = libwebauthn::transport::hid::list_devices().await.unwrap(); - if hid_devices.is_empty() { - let state = UsbState::Waiting; - self.usb_state = state.clone(); - Ok(state) - } else if hid_devices.len() == 1 { - Ok(UsbState::Connected(hid_devices.swap_remove(0))) - } else { - Ok(UsbState::SelectingDevice(hid_devices)) + loop { + tracing::debug!("current usb state: {:?}", state); + let prev_usb_state = state; + let next_usb_state = match prev_usb_state { + UsbStateInternal::Idle | UsbStateInternal::Waiting => { + let mut hid_devices = + libwebauthn::transport::hid::list_devices().await.unwrap(); + if hid_devices.is_empty() { + let state = UsbStateInternal::Waiting; + Ok(state) + } else if hid_devices.len() == 1 { + Ok(UsbStateInternal::Connected(hid_devices.swap_remove(0))) + } else { + Ok(UsbStateInternal::SelectingDevice(hid_devices)) + } } - } - 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_runtime::get().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; + UsbStateInternal::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 { + }); + } + let mut state = UsbStateInternal::Idle; + while let Some(msg) = blinking_rx.recv().await { + expected_answers -= 1; + match msg { + Some(device) => { + state = UsbStateInternal::Connected(device); break; - } else { - continue; + } + None => { + if expected_answers == 0 { + break; + } else { + continue; + } } } } + Ok(state) } - 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_runtime::get().spawn(async move { - let (mut channel, state_rx) = device.channel().await.unwrap(); - tokio_runtime::get().spawn(async move { - handle_usb_updates(signal_tx, pin_rx, state_rx).await; - debug!("Reached end of USB update task"); + UsbStateInternal::Connected(device) => { + let signal_tx2 = signal_tx.clone(); + let cred_request = cred_request.clone(); + tokio::spawn(async move { + handle_events(&cred_request, device, &signal_tx2).await; }); - match cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => { - loop { - match channel.webauthn_make_credential(&make_cred_request).await { - Ok(response) => { - handler - .notify_ceremony_completed( - AuthenticatorResponse::CredentialCreated(response), - ) - .await; - break; - } - Err(WebAuthnError::Ctap(ctap_error)) - if ctap_error.is_retryable_user_error() => - { - warn!("Retrying WebAuthn make credential operation"); - continue; - } - Err(err) => { - handler.notify_ceremony_failed(err.to_string()).await; - break; - } - }; - } + match signal_rx.recv().await { + Some(Ok(UsbUvMessage::NeedsPin { + attempts_left, + pin_tx, + })) => Ok(UsbStateInternal::NeedsPin { + attempts_left, + pin_tx: Arc::new(pin_tx), + }), + Some(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })) => { + Ok(UsbStateInternal::NeedsUserVerification { + attempts_left: attempts_left, + }) } - CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => { - loop { - match channel.webauthn_get_assertion(&get_cred_request).await { - Ok(response) => { - handler - .notify_ceremony_completed( - AuthenticatorResponse::CredentialsAsserted( - response, - ), - ) - .await; - break; - } - Err(WebAuthnError::Ctap(ctap_error)) - if ctap_error.is_retryable_user_error() => - { - warn!("Retrying WebAuthn get credential operation"); - continue; - } - Err(err) => { - handler.notify_ceremony_failed(err.to_string()).await; - break; - } - }; - } + Some(Ok(UsbUvMessage::NeedsUserPresence)) => { + Ok(UsbStateInternal::NeedsUserPresence) } - }; - }); - match self.usb_uv_handler.wait_for_notification().await { - Ok(UsbUvMessage::NeedsPin { attempts_left }) => { - Ok(UsbState::NeedsPin { attempts_left }) - } - Ok(UsbUvMessage::NeedsUserVerification { attempts_left }) => { - Ok(UsbState::NeedsUserVerification { attempts_left }) - } - Ok(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), - Ok(UsbUvMessage::ReceivedCredential(response)) => { - match response { - AuthenticatorResponse::CredentialCreated(r) => { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace( - CredentialResponse::CreatePublicKeyCredentialResponse( - MakeCredentialResponseInternal::new( - r, - vec![String::from("usb")], - String::from("cross-platform"), - ), - ), - ); - Ok(UsbState::Completed) - } - AuthenticatorResponse::CredentialsAsserted(r) => { - // at least one credential is returned from the authenticator - assert!(!r.assertions.is_empty()); - if r.assertions.len() == 1 { - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.replace( - CredentialResponse::GetPublicKeyCredentialResponse( - GetAssertionResponseInternal::new( - r.assertions[0].clone(), - String::from("cross-platform"), - ), - ), - ); - Ok(UsbState::Completed) - } else { - todo!("need to support selection from multiple credentials"); - } - } + Some(Ok(UsbUvMessage::ReceivedCredential(response))) => { + Ok(UsbStateInternal::Completed(response.clone())) } + Some(Err(err)) => Err(err.clone()), + None => Err("Channel disconnected".to_string()), } - Err(err) => Err(err), } - } - UsbState::NeedsPin { - attempts_left: Some(attempts_left), - } if attempts_left <= 1 => Err("No more USB attempts left".to_string()), - UsbState::NeedsUserVerification { - attempts_left: Some(attempts_left), - } if attempts_left <= 1 => { - Err("No more on-device user device attempts left".to_string()) - } - UsbState::NeedsPin { .. } - | UsbState::NeedsUserVerification { .. } - | UsbState::NeedsUserPresence => { - match self.usb_uv_handler.check_notification().await? { - Some(UsbUvMessage::NeedsPin { attempts_left }) => { - Ok(UsbState::NeedsPin { attempts_left }) - } - Some(UsbUvMessage::NeedsUserVerification { attempts_left }) => { - Ok(UsbState::NeedsUserVerification { attempts_left }) - } - Some(UsbUvMessage::NeedsUserPresence) => Ok(UsbState::NeedsUserPresence), - Some(UsbUvMessage::ReceivedCredential(response)) => { - match response { - AuthenticatorResponse::CredentialCreated(r) => { - self.usb_state = UsbState::Completed; - Ok(CredentialResponse::CreatePublicKeyCredentialResponse( - MakeCredentialResponseInternal::new( - r, - vec![String::from("usb")], - String::from("cross-platform"), - ), - )) - } - AuthenticatorResponse::CredentialsAsserted(r) => { - // at least one credential is returned from the authenticator - assert!(!r.assertions.is_empty()); - if r.assertions.len() == 1 { - Ok(CredentialResponse::GetPublicKeyCredentialResponse( - GetAssertionResponseInternal::new( - r.assertions[0].clone(), - String::from("cross-platform"), - ) - )) - } else { - todo!("need to support selection from multiple credentials"); - } - } + UsbStateInternal::NeedsPin { + attempts_left: Some(attempts_left), + .. + } if attempts_left <= 1 => Err("No more USB attempts left".to_string()), + UsbStateInternal::NeedsUserVerification { + attempts_left: Some(attempts_left), + } if attempts_left <= 1 => { + Err("No more on-device user device attempts left".to_string()) + } + UsbStateInternal::NeedsPin { .. } + | UsbStateInternal::NeedsUserVerification { .. } + | UsbStateInternal::NeedsUserPresence => match signal_rx.try_recv() { + Ok(msg) => match msg? { + UsbUvMessage::NeedsPin { + attempts_left, + pin_tx, + } => Ok(UsbStateInternal::NeedsPin { + attempts_left, + pin_tx: Arc::new(pin_tx), + }), + UsbUvMessage::NeedsUserVerification { attempts_left } => { + Ok(UsbStateInternal::NeedsUserVerification { + attempts_left: attempts_left, + }) } + UsbUvMessage::NeedsUserPresence => Ok(UsbStateInternal::NeedsUserPresence), + UsbUvMessage::ReceivedCredential(response) => { + Ok(UsbStateInternal::Completed(response.clone())) + } + }, + Err(TryRecvError::Empty) => Ok(prev_usb_state), + Err(TryRecvError::Disconnected) => { + Err("USB UV handler channel closed".to_string()) } - None => Ok(prev_usb_state), - } + }, + UsbStateInternal::Completed(_) => Ok(prev_usb_state), + }; + state = next_usb_state?; + tx.send(state.clone()) + .await + .map_err(|_| "Receiver channel closed".to_string())?; + match state { + UsbStateInternal::Completed(_) => break Ok(()), + _ => {} } - UsbState::Completed => Ok(prev_usb_state), - }?; - - self.usb_state = next_usb_state.clone(); - Ok(next_usb_state) + } } - /* + /* async fn cancel_device_discovery_usb(&mut self) -> Result<(), String> { *self.usb_state.lock().await = UsbState::Idle; println!("frontend: Cancel USB request"); @@ -270,24 +174,123 @@ impl Stream for LocalUsbStateStream { } */ - /* - async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { - let current_state = self.usb_state.lock().await.clone(); - match current_state { - UsbState::NeedsPin { - attempts_left: Some(attempts_left), - } if attempts_left > 1 => { - self.usb_uv_handler.send_pin(pin).await; - Ok(()) + /* + async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { + let current_state = self.usb_state.lock().await.clone(); + match current_state { + UsbState::NeedsPin { + attempts_left: Some(attempts_left), + } if attempts_left > 1 => { + self.usb_uv_handler.send_pin(pin).await; + Ok(()) + } + _ => Err(()), + } + } + */ +} + +async fn handle_events( + cred_request: &CredentialRequest, + mut device: HidDevice, + signal_tx: &Sender>, +) { + let (mut channel, state_rx) = device.channel().await.unwrap(); + let signal_tx2 = signal_tx.clone().downgrade(); + tokio::spawn(async move { + handle_usb_updates(&signal_tx2, state_rx).await; + debug!("Reached end of USB update task"); + }); + match cred_request { + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => loop { + match channel.webauthn_make_credential(&make_cred_request).await { + Ok(response) => { + notify_ceremony_completed( + &signal_tx, + AuthenticatorResponse::CredentialCreated(response), + ) + .await; + break; + } + Err(WebAuthnError::Ctap(ctap_error)) if ctap_error.is_retryable_user_error() => { + warn!("Retrying WebAuthn make credential operation"); + continue; + } + Err(err) => { + notify_ceremony_failed(&signal_tx, err.to_string()).await; + break; + } + }; + }, + CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => loop { + match channel.webauthn_get_assertion(&get_cred_request).await { + Ok(response) => { + notify_ceremony_completed( + &signal_tx, + AuthenticatorResponse::CredentialsAsserted(response), + ) + .await; + break; + } + Err(WebAuthnError::Ctap(ctap_error)) if ctap_error.is_retryable_user_error() => { + warn!("Retrying WebAuthn get credential operation"); + continue; + } + Err(err) => { + notify_ceremony_failed(&signal_tx, err.to_string()).await; + break; + } + }; + }, + }; +} + +async fn notify_ceremony_completed( + signal_tx: &Sender>, + response: AuthenticatorResponse, +) { + signal_tx + .send(Ok(UsbUvMessage::ReceivedCredential(response))) + .await + .unwrap(); +} + +async fn notify_ceremony_failed(signal_tx: &Sender>, err: String) { + signal_tx.send(Err(err)).await.unwrap(); +} + +impl UsbHandler for LocalUsbHandler { + type Stream = LocalUsbStateStream; + + fn start(&self, request: &CredentialRequest) -> Self::Stream { + let request = request.clone(); + let (tx, rx) = mpsc::channel(32); + tokio::spawn(async move { + if let Err(err) = LocalUsbHandler::process(tx, request).await { + tracing::error!("Error getting credential from USB: {:?}", err); } - _ => Err(()), - } + }); + LocalUsbStateStream { rx } + } +} + +pub struct LocalUsbStateStream { + rx: Receiver, +} + +impl Stream for LocalUsbStateStream { + type Item = UsbStateInternal; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.deref_mut().rx.poll_recv(cx) } - */ } #[derive(Clone, Debug, Default)] -pub enum UsbState { +pub enum UsbStateInternal { /// Not polling for FIDO USB device. #[default] Idle, @@ -301,6 +304,7 @@ pub enum UsbState { /// The device needs the PIN to be entered. NeedsPin { attempts_left: Option, + pin_tx: Arc>, }, /// The device needs on-device user verification. @@ -312,7 +316,7 @@ pub enum UsbState { NeedsUserPresence, /// USB tapped, received credential - Completed, + Completed(AuthenticatorResponse), // TODO: implement cancellation // This isn't actually sent from the server. //UserCancelled, @@ -322,76 +326,76 @@ pub enum UsbState { SelectingDevice(Vec), } -#[derive(Clone, Debug)] -pub struct UsbUvHandler { - signal_tx: async_std::channel::Sender>, - signal_rx: async_std::channel::Receiver>, - pin_tx: async_std::channel::Sender, - pin_rx: async_std::channel::Receiver, -} - -impl UsbUvHandler { - pub fn new() -> Self { - let (signal_tx, signal_rx) = async_std::channel::unbounded(); - let (pin_tx, pin_rx) = async_std::channel::unbounded(); - UsbUvHandler { - signal_tx, - signal_rx, - pin_tx, - pin_rx, - } - } -} -trait UsbUvHandlerTrait { - async fn notify_ceremony_completed(&self, response: AuthenticatorResponse); - - async fn notify_ceremony_failed(&self, err: String); +#[derive(Clone, Debug, Default)] +pub enum UsbState { + /// Not polling for FIDO USB device. + #[default] + Idle, - async fn send_pin(&self, pin: &str); + /// Awaiting FIDO USB device to be plugged in. + Waiting, - async fn wait_for_notification(&self) -> Result; + /// USB device connected, prompt user to tap + Connected, - async fn check_notification(&self) -> Result, String>; -} + /// The device needs the PIN to be entered. + NeedsPin { + attempts_left: Option, + pin_tx: Arc>, + }, -impl UsbUvHandlerTrait for UsbUvHandler { - async fn notify_ceremony_completed(&self, response: AuthenticatorResponse) { - self.signal_tx - .send(Ok(UsbUvMessage::ReceivedCredential(response))) - .await - .unwrap(); - } + /// The device needs on-device user verification. + NeedsUserVerification { + attempts_left: Option, + }, - async fn notify_ceremony_failed(&self, err: String) { - self.signal_tx.send(Err(err)).await.unwrap(); - } + /// The device needs evidence of user presence (e.g. touch) to release the credential. + NeedsUserPresence, - async fn send_pin(&self, pin: &str) { - self.pin_tx.send(pin.to_owned()).await.unwrap(); - } + /// USB tapped, received credential + Completed, + // TODO: implement cancellation + // This isn't actually sent from the server. + //UserCancelled, - async fn wait_for_notification(&self) -> Result { - match self.signal_rx.recv().await { - Ok(msg) => msg, - Err(err) => Err(err.to_string()), - } - } + // When we encounter multiple devices, we let all of them blink and continue + // with the one that was tapped. + SelectingDevice, +} - async fn check_notification(&self) -> Result, String> { - match self.signal_rx.try_recv() { - Ok(msg) => Ok(Some(msg?)), - Err(TryRecvError::Empty) => Ok(None), - Err(TryRecvError::Closed) => Err("USB UV handler channel closed".to_string()), +impl From for UsbState { + fn from(value: UsbStateInternal) -> Self { + match value { + UsbStateInternal::Idle => UsbState::Idle, + UsbStateInternal::Waiting => UsbState::Waiting, + UsbStateInternal::Connected(_) => UsbState::Connected, + UsbStateInternal::NeedsPin { + attempts_left, + pin_tx, + } => UsbState::NeedsPin { + attempts_left, + pin_tx, + }, + UsbStateInternal::NeedsUserVerification { attempts_left } => { + UsbState::NeedsUserVerification { attempts_left } + } + UsbStateInternal::NeedsUserPresence => UsbState::NeedsUserPresence, + UsbStateInternal::Completed(_) => UsbState::Completed, + // UsbStateInternal::UserCancelled => UsbState:://UserCancelled, + UsbStateInternal::SelectingDevice(_) => UsbState::SelectingDevice, } } } async fn handle_usb_updates( - signal_tx: async_std::channel::Sender>, - pin_rx: async_std::channel::Receiver, - mut state_rx: tokio::sync::mpsc::Receiver, + signal_tx: &WeakSender>, + mut state_rx: Receiver, ) { while let Some(msg) = state_rx.recv().await { + let signal_tx = match signal_tx.upgrade() { + Some(tx) => tx, + None => break, + }; match msg { UxUpdate::UvRetry { attempts_left } => { signal_tx @@ -405,16 +409,20 @@ async fn handle_usb_updates( signal_tx.send(Err("No more PIN attempts allowed. Select a different authenticator or try again later.".to_string())).await.unwrap(); continue; } + let (pin_tx, pin_rx) = oneshot::channel(); signal_tx .send(Ok(UsbUvMessage::NeedsPin { + pin_tx, attempts_left: pin_update.attempts_left, })) .await .unwrap(); - if let Ok(pin) = pin_rx.recv().await { - pin_update.send_pin(&pin).unwrap(); - } else { - debug!("PIN channel closed."); + match pin_rx.await { + Ok(pin) => match pin_update.send_pin(&pin) { + Ok(()) => {} + Err(err) => tracing::error!("Error sending pin to device: {:?}", err), + }, + Err(err) => tracing::debug!("Error receiving pin from client: {:?}", err), } } UxUpdate::PresenceRequired => { @@ -429,8 +437,13 @@ async fn handle_usb_updates( } enum UsbUvMessage { - NeedsPin { attempts_left: Option }, - NeedsUserVerification { attempts_left: Option }, + NeedsPin { + attempts_left: Option, + pin_tx: oneshot::Sender, + }, + NeedsUserVerification { + attempts_left: Option, + }, NeedsUserPresence, ReceivedCredential(AuthenticatorResponse), } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 4a2bc0e5..a21f369b 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -19,16 +19,21 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialUserEntity, }; use ring::digest; -use tokio::sync::{mpsc::{self, Receiver, Sender}, Mutex as AsyncMutex}; +use tokio::sync::{ + mpsc::{self, Receiver, Sender}, + Mutex as AsyncMutex, +}; use zbus::{ connection::{self, Connection}, - fdo, interface, Result, - zvariant::{DeserializeDict, SerializeDict, Type} + fdo, interface, + zvariant::{DeserializeDict, SerializeDict, Type}, + Result, }; use crate::application::ExampleApplication; use crate::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; use crate::credential_service::hybrid::InternalHybridHandler; +use crate::credential_service::usb::LocalUsbHandler; use crate::credential_service::CredentialService; use crate::view_model::CredentialType; use crate::view_model::Operation; @@ -69,6 +74,7 @@ fn start_gui_thread(mut rx: Receiver<(CredentialRequest, Sender, rx_update: async_std::channel::Receiver) { +fn start_gtk_app( + tx_event: async_std::channel::Sender, + rx_update: async_std::channel::Receiver, +) { // Prepare i18n gettextrs::setlocale(LocaleCategory::LcAll, ""); gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index 8740a65e..a89013be 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -6,7 +6,6 @@ mod cose; mod credential_service; mod dbus; mod serde; -mod tokio_runtime; #[allow(dead_code)] mod view_model; mod webauthn; @@ -21,7 +20,6 @@ async fn main() { rustls::crypto::ring::default_provider() .install_default() .expect("Failed to install rustls crypto provider"); - _ = tokio_runtime::get(); println!("Starting..."); run().await.unwrap(); diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/tokio_runtime.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/tokio_runtime.rs deleted file mode 100644 index 072fe98b..00000000 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/tokio_runtime.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::sync::OnceLock; - -use tokio::runtime::Runtime; - -static RUNTIME: OnceLock = OnceLock::new(); - -pub fn get() -> &'static Runtime { - RUNTIME.get_or_init(|| Runtime::new().expect("Tokio runtime to start")) -} 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 419cd0b0..a6076c8d 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 @@ -1,23 +1,22 @@ pub mod gtk; -use std::marker::PhantomData; use std::sync::Arc; -use std::time::Duration; use async_std::prelude::*; use async_std::{ channel::{Receiver, Sender}, sync::Mutex, }; -use tracing::info; +use tokio::sync::oneshot; +use tracing::{error, info}; -// TODO: turn CredentialService into a trait so we don't have to do specify handler types manually. -use crate::credential_service::hybrid::InternalHybridHandler; -use crate::credential_service::{CredentialService, CredentialServiceClient}; +use crate::credential_service::{CredentialServiceClient, UsbState}; #[derive(Debug)] pub(crate) struct ViewModel -where C: CredentialServiceClient + Send { +where + C: CredentialServiceClient + Send, +{ credential_service: Arc>, tx_update: Sender, rx_event: Receiver, @@ -33,8 +32,7 @@ where C: CredentialServiceClient + Send { providers: Vec, - usb_device_state: UsbState, - usb_device_pin_state: UsbPinState, + usb_pin_tx: Option>>, hybrid_qr_state: HybridState, hybrid_qr_code_data: Option>, @@ -42,7 +40,7 @@ where C: CredentialServiceClient + Send { hybrid_linked_state: HybridState, } -impl ViewModel { +impl ViewModel { pub(crate) fn new( operation: Operation, credential_service: C, @@ -62,8 +60,7 @@ impl ViewModel { selected_device: None, selected_credential: None, providers: Vec::new(), - usb_device_state: UsbState::default(), - usb_device_pin_state: UsbPinState::default(), + usb_pin_tx: None, hybrid_qr_state: HybridState::default(), hybrid_qr_code_data: None, hybrid_linked_state: HybridState::default(), @@ -145,18 +142,14 @@ impl ViewModel { return; } match prev_device.transport { - Transport::Usb => self - .credential_service - .lock() - .await - .cancel_device_discovery_usb() - .await - .unwrap(), + Transport::Usb => { + todo!("Implement cancellation for USB"); + } Transport::HybridQr => { todo!("Implement cancellation for Hybrid QR"); } _ => { - todo!() + todo!(); } }; self.selected_credential = None; @@ -167,36 +160,42 @@ impl ViewModel { Transport::Usb => { let cred_service = self.credential_service.clone(); let tx = self.bg_update.clone(); + let mut stream = { + let cred_service = cred_service.lock().await; + cred_service.get_usb_credential() + }; async_std::task::spawn(async move { // TODO: add cancellation - let mut prev_state = UsbState::default(); - loop { - match cred_service.lock().await.poll_device_discovery_usb().await { - Ok(usb_state) => { - let state = usb_state.into(); - if prev_state != state { - println!("{:?}", state); - tx.send(BackgroundEvent::UsbStateChanged(state.clone())) - .await - .unwrap(); - } - prev_state = state; - match prev_state { - UsbState::Completed => break, - UsbState::UserCancelled => break, - _ => {} - }; - async_std::task::sleep(Duration::from_millis(50)).await; - } - Err(err) => { - // TODO: move to error page - tracing::error!( - "There was an error trying to get credentials from USB: {}", - err - ); - break; + while let Some(usb_state) = stream.next().await { + tx.send(BackgroundEvent::UsbStateChanged(usb_state.clone())) + .await + .unwrap(); + /* + Ok(usb_state) => { + let state = usb_state.into(); + if prev_state != state { + println!("{:?}", state); + tx.send(BackgroundEvent::UsbStateChanged(state.clone())) + .await + .unwrap(); } - }; + prev_state = state; + match prev_state { + UsbState::Completed => break, + UsbState::UserCancelled => break, + _ => {} + }; + async_std::task::sleep(Duration::from_millis(50)).await; + } + Err(err) => { + // TODO: move to error page + tracing::error!( + "There was an error trying to get credentials from USB: {}", + err + ); + break; + } + */ } }); } @@ -264,12 +263,17 @@ impl ViewModel { println!("Selected device {id}"); } Event::View(ViewEvent::UsbPinEntered(pin)) => { - self.credential_service - .lock() - .await - .validate_usb_device_pin(&pin) - .await - .unwrap(); + let pin_tx = self.usb_pin_tx.take().unwrap(); + match Arc::try_unwrap(pin_tx) { + Ok(pin_tx) => { + if let Err(_) = pin_tx.send(pin) { + error!("Failed to send pin to device"); + } + } + Err(e) => { + error!("Failed to send pin to device: {:?}", e); + } + } } Event::View(ViewEvent::CredentialSelected(cred_id)) => { println!( @@ -287,13 +291,17 @@ impl ViewModel { println!("UsbPressed"); } Event::Background(BackgroundEvent::UsbStateChanged(state)) => { - self.usb_device_state = state; - match self.usb_device_state { + // TODO: do we need to store the USB state? + match state { UsbState::Connected => { info!("Found USB device") } - UsbState::NeedsPin { attempts_left } => { + UsbState::NeedsPin { + attempts_left, + pin_tx, + } => { + let _ = self.usb_pin_tx.insert(pin_tx); self.tx_update .send(ViewUpdate::UsbNeedsPin { attempts_left }) .await @@ -321,7 +329,7 @@ impl ViewModel { .await .unwrap(); } - UsbState::NotListening | UsbState::Waiting | UsbState::UserCancelled => {} + UsbState::Idle | UsbState::Waiting => {} } } Event::Background(BackgroundEvent::HybridQrStateChanged(state)) => { @@ -530,75 +538,5 @@ impl Transport { } } -#[derive(Clone, Debug, Default, PartialEq)] -pub enum UsbState { - /// Not currently listening for USB devices. - #[default] - NotListening, - - /// Awaiting FIDO USB device to be plugged in. - Waiting, - - /// The device needs the PIN to be entered. - NeedsPin { - attempts_left: Option, - }, - - /// The device needs on-device user verification to be entered. - NeedsUserVerification { - attempts_left: Option, - }, - - /// The device needs on-device user verification to be entered. - NeedsUserPresence, - - /// USB device connected, prompt user to tap - Connected, - - /// USB tapped, received credential - Completed, - - // 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::NeedsPin { attempts_left } => { - UsbState::NeedsPin { attempts_left } - } - crate::credential_service::UsbState::NeedsUserVerification { attempts_left } => { - UsbState::NeedsUserVerification { attempts_left } - } - crate::credential_service::UsbState::NeedsUserPresence => UsbState::NeedsUserPresence, - crate::credential_service::UsbState::Completed => UsbState::Completed, - } - } -} - -#[derive(Debug, Default)] -pub enum UsbPinState { - #[default] - Waiting, - - PinIncorrect { - attempts_left: u32, - }, - - LockedOut { - unlock_time: Duration, - }, - - PinCorrect, -} - #[derive(Debug, Default)] pub struct UserVerificationMethod; From a45b1169e1c13d8b61e181ec0a10756cf10855f5 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 4 Jun 2025 14:11:03 -0500 Subject: [PATCH 09/13] wip: credential service to a separate thread --- .vscode/launch.json | 2 +- .../Cargo.lock | 25 +- .../Cargo.toml | 1 + .../src/credential_service/hybrid.rs | 38 ++- .../src/credential_service/mod.rs | 76 ++--- .../src/credential_service/server.rs | 275 ++++++++++++++++++ .../src/credential_service/usb.rs | 94 +++--- .../src/dbus.rs | 177 +++++------ .../src/gui.rs | 71 +++++ .../src/main.rs | 29 +- .../src/view_model/mod.rs | 26 +- 11 files changed, 605 insertions(+), 209 deletions(-) create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/src/gui.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index ba544447..3d7c7926 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "args": [], "env": { "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/data", - "RUST_LOG": "xyz_iinuwa_credential_manager_portal_gtk=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug" + "RUST_LOG": "xyz_iinuwa_credential_manager_portal_gtk=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug" }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock index ada739b6..68a06f25 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock @@ -274,6 +274,28 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-task" version = "4.7.1" @@ -1900,7 +1922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3978,6 +4000,7 @@ name = "xyz-iinuwa-credential-manager-portal-gtk" version = "0.1.0" dependencies = [ "async-std", + "async-stream", "async-trait", "base64", "cosey", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml index 5a36ee30..7feec3fb 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml @@ -29,3 +29,4 @@ qrcode = "0.14.1" # this is temporary until we move COSE -> Vec serialization methods into libwebauthn cosey = "0.3.2" rustls = { version = "0.23.27", default-features = false, features = ["std", "tls12", "ring", "log", "logging", "prefer-post-quantum"] } +async-stream = "0.3.6" diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index cda29131..b64de859 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -2,6 +2,7 @@ use std::fmt::Debug; use std::task::Poll; use async_std::channel::Receiver; +use async_stream::stream; use futures_lite::{FutureExt, Stream}; use libwebauthn::fido::{AuthenticatorData, AuthenticatorDataFlags}; use libwebauthn::ops::webauthn::{Assertion, GetAssertionResponse}; @@ -9,14 +10,18 @@ use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transpo use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint}; use libwebauthn::transport::Device; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; +use tokio::sync::mpsc::error::TryRecvError; use crate::dbus::CredentialRequest; use super::AuthenticatorResponse; pub(crate) trait HybridHandler { - type Stream: Stream; - fn start(&self, request: &CredentialRequest) -> Self::Stream; + // type Stream: Stream; + fn start( + &self, + request: &CredentialRequest, + ) -> impl Stream + Unpin + Send + Sized + 'static; } #[derive(Debug)] @@ -28,9 +33,12 @@ impl InternalHybridHandler { } impl HybridHandler for InternalHybridHandler { - type Stream = InternalHybridStream; + // type Stream = InternalHybridStream; - fn start(&self, request: &CredentialRequest) -> Self::Stream { + fn start( + &self, + request: &CredentialRequest, + ) -> impl Stream + Unpin + Send + Sized + 'static { let request = request.clone(); let (tx, rx) = async_std::channel::unbounded(); tokio::spawn(async move { @@ -92,10 +100,16 @@ impl HybridHandler for InternalHybridHandler { } }); }); - InternalHybridStream { rx } + Box::pin(stream! { + while let Ok(state) = rx.recv().await { + yield state + } + }) + // InternalHybridStream { rx } } } +/* pub struct InternalHybridStream { rx: Receiver, } @@ -107,14 +121,19 @@ impl Stream for InternalHybridStream { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - match self.rx.recv().poll(cx) { + match self.rx.poll() { + Ok(state) => Poll::Ready(state), + Err(_) => Poll:Ready(None), + /* Poll::Pending => Poll::Pending, Poll::Ready(Ok(state)) => Poll::Ready(Some(state)), Poll::Ready(Err(_)) => Poll::Ready(None), + */ } } } +*/ #[derive(Debug)] pub struct DummyHybridHandler { stream: DummyHybridStateStream, @@ -137,9 +156,12 @@ impl Default for DummyHybridHandler { } } impl HybridHandler for DummyHybridHandler { - type Stream = DummyHybridStateStream; + // type Stream = DummyHybridStateStream; - fn start(&self, _request: &CredentialRequest) -> Self::Stream { + fn start( + &self, + _request: &CredentialRequest, + ) -> impl Stream + Send + Sized + Unpin + 'static { self.stream.clone() } } 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 3a511d9e..cd09ea04 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,4 +1,5 @@ pub mod hybrid; +mod server; pub mod usb; use std::{ @@ -23,14 +24,17 @@ use crate::{ }; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; -pub use usb::UsbState; use usb::{UsbHandler, UsbStateInternal}; +pub use { + server::{CredentialManagementClient, CredentialServiceClient, InProcessServer}, + usb::UsbState, +}; #[derive(Debug)] pub struct CredentialService { devices: Vec, - cred_request: CredentialRequest, + cred_request: Mutex>, // Place to store data to be returned to the caller cred_response: Arc>>, @@ -38,13 +42,12 @@ pub struct CredentialService { usb_handler: U, } -impl CredentialService { - pub fn new( - cred_request: CredentialRequest, - cred_response: Arc>>, - hybrid_handler: H, - usb_handler: U, - ) -> Self { +impl CredentialService +where +// ::Stream: Unpin + Send + Sized + 'static, +// ::Stream: Unpin + Send + Sized + 'static, +{ + pub fn new(hybrid_handler: H, usb_handler: U) -> Self { let devices = vec![ Device { id: String::from("0"), @@ -58,36 +61,32 @@ impl CredentialService { Self { devices, - cred_request, - cred_response, + cred_request: Mutex::new(None), + cred_response: Arc::new(Mutex::new(None)), hybrid_handler, usb_handler, } } -} - -pub trait CredentialServiceClient { - async fn get_available_public_key_devices(&self) -> Result, ()>; - - fn get_hybrid_credential(&self) -> Pin + Send>>; - fn get_usb_credential(&self) -> Pin + Send>>; - fn complete_auth(&mut self); -} + pub fn init_request(&self, request: &CredentialRequest) -> Result<(), String> { + let mut cred_request = self.cred_request.lock().unwrap(); + if cred_request.is_some() { + Err("Already a request in progress.".to_string()) + } else { + _ = cred_request.insert(request.clone()); + Ok(()) + } + } -impl CredentialServiceClient - for CredentialService -where - ::Stream: Unpin + Send + 'static, - ::Stream: Unpin + Send + 'static, -{ async fn get_available_public_key_devices(&self) -> Result, ()> { Ok(self.devices.to_owned()) } fn get_hybrid_credential(&self) -> Pin + Send + 'static>> { - let stream = self.hybrid_handler.start(&self.cred_request); + let guard = self.cred_request.lock().unwrap(); + let cred_request = guard.clone().unwrap(); + let stream = self.hybrid_handler.start(&cred_request); let cred_response = self.cred_response.clone(); Box::pin(HybridStateStream { inner: stream, @@ -96,16 +95,19 @@ where } fn get_usb_credential(&self) -> Pin + Send + 'static>> { - let stream = self.usb_handler.start(&self.cred_request); + let guard = self.cred_request.lock().unwrap(); + let cred_request = guard.clone().unwrap(); + let stream = self.usb_handler.start(&cred_request); Box::pin(UsbStateStream { inner: stream, cred_response: self.cred_response.clone(), }) } - fn complete_auth(&mut self) { - // let mut data = self.output_data.lock().unwrap(); - // data.replace((self.cred_response)); + pub fn complete_auth(&self) -> Option { + self.cred_request.lock().unwrap().take(); + let mut cred_response = self.cred_response.lock().unwrap(); + cred_response.take() } } @@ -189,7 +191,6 @@ impl AuthenticatorResponse { ), ) } - AuthenticatorResponse::CredentialsAsserted(GetAssertionResponse { assertions }) if assertions.len() == 1 => { @@ -219,7 +220,7 @@ impl From for AuthenticatorResponse { #[cfg(test)] mod test { - use std::sync::{Arc, Mutex}; + use std::sync::Arc; use async_std::stream::StreamExt; @@ -230,13 +231,12 @@ mod test { use super::{ hybrid::{DummyHybridHandler, HybridStateInternal}, - AuthenticatorResponse, CredentialService, CredentialServiceClient, + AuthenticatorResponse, CredentialService, }; #[test] fn test_hybrid_sets_credential() { let request = create_credential_request(); - let response = Arc::new(Mutex::new(None)); let qr_code = String::from("FIDO:/078241338926040702789239694720083010994762289662861130514766991835876383562063181103169246410435938367110394959927031730060360967994421343201235185697538107096654083332"); let authenticator_response = create_authenticator_response(); @@ -247,10 +247,12 @@ mod test { HybridStateInternal::Completed(authenticator_response), ]); let usb_handler = LocalUsbHandler {}; - let cred_service = CredentialService::new(request, response, hybrid_handler, usb_handler); + let cred_service = Arc::new(CredentialService::new(hybrid_handler, usb_handler)); + cred_service.init_request(&request).unwrap(); let mut stream = cred_service.get_hybrid_credential(); async_std::task::block_on(async move { while let Some(_) = stream.next().await {} }); - assert!(cred_service.cred_response.lock().unwrap().is_some()); + let cred_service = Arc::try_unwrap(cred_service).unwrap(); + assert!(cred_service.complete_auth().is_some()); } fn create_credential_request() -> CredentialRequest { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs new file mode 100644 index 00000000..7391cc1c --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs @@ -0,0 +1,275 @@ +use std::fmt::Debug; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use futures_lite::Stream; +use tokio::sync::{mpsc, oneshot}; + +use crate::dbus::{CredentialRequest, CredentialResponse}; +use crate::view_model::Device; + +use super::hybrid::{HybridHandler, HybridState}; +use super::usb::{UsbHandler, UsbState}; +use super::CredentialService; + +pub enum ServiceRequest { + GetDevices, + GetHybridCredential, + GetUsbCredential, +} + +enum ManagementRequest { + InitRequest(CredentialRequest), + CompleteAuth, +} + +#[derive(Debug)] +enum ManagementResponse { + InitRequest(Result<(), String>), + CompleteAuth(Option), +} + +pub enum ServiceResponse { + GetDevices(Vec), + GetHybridCredential(Pin + Send>>), + GetUsbCredential(Pin + Send>>), +} + +impl Debug for ServiceResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::GetDevices(arg0) => f.debug_tuple("GetDevices").field(arg0).finish(), + Self::GetHybridCredential(_) => f + .debug_tuple("GetHybridCredential") + .field(&String::from("")) + .finish(), + Self::GetUsbCredential(_) => f + .debug_tuple("GetUsbCredential") + .field(&String::from("")) + .finish(), + } + } +} + +enum InProcessServerRequest { + Client(ServiceRequest), + Management(ManagementRequest), +} + +#[derive(Debug)] +enum InProcessServerResponse { + Client(ServiceResponse), + Management(ManagementResponse), +} + +pub trait CredentialServiceClient { + fn get_available_public_key_devices( + &self, + ) -> impl Future, ()>> + Send; + + fn get_hybrid_credential( + &self, + ) -> impl Future + Send>>> + Send; + fn get_usb_credential( + &self, + ) -> impl Future + Send>>> + Send; +} + +pub trait CredentialManagementClient { + fn init_request( + &self, + cred_request: CredentialRequest, + ) -> impl Future> + Send; + fn complete_auth(&self) -> impl Future> + Send; +} + +pub struct InProcessManager { + tx: mpsc::Sender<( + InProcessServerRequest, + oneshot::Sender, + )>, +} + +impl InProcessManager { + async fn send(&self, request: ManagementRequest) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + self.tx + .send((InProcessServerRequest::Management(request), response_tx)) + .await + .unwrap(); + match response_rx.await { + Ok(InProcessServerResponse::Management(response)) => Ok(response), + Ok(_) => { + tracing::error!("invalid response received from server"); + Err(()) + } + Err(err) => { + tracing::error!("Failed to retrieve response from server: {:?}", err); + Err(()) + } + } + } +} + +impl CredentialManagementClient for InProcessManager { + async fn init_request(&self, cred_request: CredentialRequest) -> Result<(), String> { + let response = self + .send(ManagementRequest::InitRequest(cred_request)) + .await + .unwrap(); + if let ManagementResponse::InitRequest(result) = response { + result + } else { + Err("No credentials in credential service".to_string()) + } + } + + async fn complete_auth(&self) -> Result { + let response = self.send(ManagementRequest::CompleteAuth).await.unwrap(); + if let ManagementResponse::CompleteAuth(Some(cred_response)) = response { + Ok(cred_response) + } else { + Err("No credentials in credential service".to_string()) + } + } +} + +pub struct InProcessClient { + tx: mpsc::Sender<( + InProcessServerRequest, + oneshot::Sender, + )>, +} + +impl InProcessClient { + async fn send(&self, request: ServiceRequest) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + self.tx + .send((InProcessServerRequest::Client(request), response_tx)) + .await + .unwrap(); + match response_rx.await { + Ok(InProcessServerResponse::Client(response)) => Ok(response), + Ok(_) => { + tracing::error!("invalid response received from server"); + Err(()) + } + Err(err) => { + tracing::error!("Failed to retrieve response from server: {:?}", err); + Err(()) + } + } + } +} + +impl CredentialServiceClient for InProcessClient { + async fn get_available_public_key_devices(&self) -> Result, ()> { + let response = self.send(ServiceRequest::GetDevices).await.unwrap(); + if let ServiceResponse::GetDevices(devices) = response { + Ok(devices) + } else { + Err(()) + } + } + + async fn get_hybrid_credential(&self) -> Pin + Send>> { + let response = self + .send(ServiceRequest::GetHybridCredential) + .await + .unwrap(); + if let ServiceResponse::GetHybridCredential(stream) = response { + stream + } else { + panic!("Unable to get hybrid credential"); + } + } + + async fn get_usb_credential(&self) -> Pin + Send>> { + let response = self.send(ServiceRequest::GetUsbCredential).await.unwrap(); + if let ServiceResponse::GetUsbCredential(stream) = response { + stream + } else { + panic!("Unable to get usb credential"); + } + } +} + +impl CredentialServiceClient for Arc { + fn get_available_public_key_devices(&self) -> impl Future, ()>> { + InProcessClient::get_available_public_key_devices(&self) + } + + fn get_hybrid_credential( + &self, + ) -> impl Future + Send>>> { + InProcessClient::get_hybrid_credential(&self) + } + + fn get_usb_credential( + &self, + ) -> impl Future + Send>>> { + InProcessClient::get_usb_credential(&self) + } +} + +#[derive(Debug)] +pub struct InProcessServer +where + H: HybridHandler + Debug, + // ::Stream: Unpin + Send + Sized + 'static, + U: UsbHandler + Debug, + // ::Stream: Unpin + Send + Sized + 'static, +{ + svc: CredentialService, + rx: mpsc::Receiver<( + InProcessServerRequest, + oneshot::Sender, + )>, +} + +impl InProcessServer +where + H: HybridHandler + Debug, + // ::Stream: Unpin + Send + Sized + 'static, + U: UsbHandler + Debug, + // ::Stream: Unpin + Send + Sized + 'static, +{ + pub fn new(svc: CredentialService) -> (Self, InProcessManager, InProcessClient) { + let (tx, rx) = mpsc::channel(256); + + let mgr_tx = tx.clone(); + let mgr = InProcessManager { tx: mgr_tx }; + let client_tx = tx.clone(); + let client = InProcessClient { tx: client_tx }; + (Self { svc, rx }, mgr, client) + } + + pub async fn run(&mut self) { + while let Some((request, tx)) = self.rx.recv().await { + let response = match request { + InProcessServerRequest::Client(ServiceRequest::GetDevices) => { + let rsp = self.svc.get_available_public_key_devices().await.unwrap(); + InProcessServerResponse::Client(ServiceResponse::GetDevices(rsp)) + } + InProcessServerRequest::Client(ServiceRequest::GetHybridCredential) => { + let rsp = self.svc.get_hybrid_credential(); + InProcessServerResponse::Client(ServiceResponse::GetHybridCredential(rsp)) + } + InProcessServerRequest::Client(ServiceRequest::GetUsbCredential) => { + let rsp = self.svc.get_usb_credential(); + InProcessServerResponse::Client(ServiceResponse::GetUsbCredential(rsp)) + } + InProcessServerRequest::Management(ManagementRequest::InitRequest(request)) => { + let rsp = self.svc.init_request(&request); + InProcessServerResponse::Management(ManagementResponse::InitRequest(rsp)) + } + InProcessServerRequest::Management(ManagementRequest::CompleteAuth) => { + let rsp = self.svc.complete_auth(); + InProcessServerResponse::Management(ManagementResponse::CompleteAuth(rsp)) + } + }; + tx.send(response).unwrap() + } + } +} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs index 016ef5ce..db0d24ef 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs @@ -1,15 +1,13 @@ -use std::{ops::DerefMut, sync::Arc, time::Duration}; +use std::{ops::DerefMut, time::Duration}; +use async_stream::stream; use futures_lite::Stream; use libwebauthn::{ transport::{hid::HidDevice, Device}, webauthn::{Error as WebAuthnError, WebAuthn}, UxUpdate, }; -use tokio::sync::{ - mpsc::{self, error::TryRecvError, Receiver, Sender, WeakSender}, - oneshot, -}; +use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; use crate::dbus::CredentialRequest; @@ -17,8 +15,12 @@ use crate::dbus::CredentialRequest; use super::AuthenticatorResponse; pub(crate) trait UsbHandler { - type Stream: Stream; - fn start(&self, request: &CredentialRequest) -> Self::Stream; + // type Stream: Stream; + // fn start(&self, request: &CredentialRequest) -> Self::Stream; + fn start( + &self, + request: &CredentialRequest, + ) -> impl Stream + Send + Sized + Unpin + 'static; } #[derive(Debug)] @@ -101,7 +103,7 @@ impl LocalUsbHandler { pin_tx, })) => Ok(UsbStateInternal::NeedsPin { attempts_left, - pin_tx: Arc::new(pin_tx), + pin_tx: pin_tx, }), Some(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })) => { Ok(UsbStateInternal::NeedsUserVerification { @@ -129,29 +131,31 @@ impl LocalUsbHandler { } UsbStateInternal::NeedsPin { .. } | UsbStateInternal::NeedsUserVerification { .. } - | UsbStateInternal::NeedsUserPresence => match signal_rx.try_recv() { - Ok(msg) => match msg? { - UsbUvMessage::NeedsPin { - attempts_left, - pin_tx, - } => Ok(UsbStateInternal::NeedsPin { - attempts_left, - pin_tx: Arc::new(pin_tx), - }), - UsbUvMessage::NeedsUserVerification { attempts_left } => { - Ok(UsbStateInternal::NeedsUserVerification { - attempts_left: attempts_left, - }) - } - UsbUvMessage::NeedsUserPresence => Ok(UsbStateInternal::NeedsUserPresence), - UsbUvMessage::ReceivedCredential(response) => { - Ok(UsbStateInternal::Completed(response.clone())) - } - }, - Err(TryRecvError::Empty) => Ok(prev_usb_state), - Err(TryRecvError::Disconnected) => { - Err("USB UV handler channel closed".to_string()) + | UsbStateInternal::NeedsUserPresence => match signal_rx.recv().await { + Some(msg) => { + let state = match msg? { + UsbUvMessage::NeedsPin { + attempts_left, + pin_tx, + } => Ok(UsbStateInternal::NeedsPin { + attempts_left, + pin_tx: pin_tx, + }), + UsbUvMessage::NeedsUserVerification { attempts_left } => { + Ok(UsbStateInternal::NeedsUserVerification { + attempts_left: attempts_left, + }) + } + UsbUvMessage::NeedsUserPresence => { + Ok(UsbStateInternal::NeedsUserPresence) + } + UsbUvMessage::ReceivedCredential(response) => { + Ok(UsbStateInternal::Completed(response.clone())) + } + }; + state } + None => Err("USB UV handler channel closed".to_string()), }, UsbStateInternal::Completed(_) => Ok(prev_usb_state), }; @@ -260,17 +264,25 @@ async fn notify_ceremony_failed(signal_tx: &Sender> } impl UsbHandler for LocalUsbHandler { - type Stream = LocalUsbStateStream; + // type Stream = LocalUsbStateStream; - fn start(&self, request: &CredentialRequest) -> Self::Stream { + fn start( + &self, + request: &CredentialRequest, + ) -> impl Stream + Send + Sized + Unpin + 'static { let request = request.clone(); - let (tx, rx) = mpsc::channel(32); + let (tx, mut rx) = mpsc::channel(32); tokio::spawn(async move { if let Err(err) = LocalUsbHandler::process(tx, request).await { tracing::error!("Error getting credential from USB: {:?}", err); } }); - LocalUsbStateStream { rx } + Box::pin(stream! { + while let Some(state) = rx.recv().await { + yield state + } + }) + // LocalUsbStateStream { rx } } } @@ -304,7 +316,7 @@ pub enum UsbStateInternal { /// The device needs the PIN to be entered. NeedsPin { attempts_left: Option, - pin_tx: Arc>, + pin_tx: mpsc::Sender, }, /// The device needs on-device user verification. @@ -341,7 +353,7 @@ pub enum UsbState { /// The device needs the PIN to be entered. NeedsPin { attempts_left: Option, - pin_tx: Arc>, + pin_tx: mpsc::Sender, }, /// The device needs on-device user verification. @@ -409,7 +421,7 @@ async fn handle_usb_updates( signal_tx.send(Err("No more PIN attempts allowed. Select a different authenticator or try again later.".to_string())).await.unwrap(); continue; } - let (pin_tx, pin_rx) = oneshot::channel(); + let (pin_tx, mut pin_rx) = mpsc::channel(1); signal_tx .send(Ok(UsbUvMessage::NeedsPin { pin_tx, @@ -417,12 +429,12 @@ async fn handle_usb_updates( })) .await .unwrap(); - match pin_rx.await { - Ok(pin) => match pin_update.send_pin(&pin) { + match pin_rx.recv().await { + Some(pin) => match pin_update.send_pin(&pin) { Ok(()) => {} Err(err) => tracing::error!("Error sending pin to device: {:?}", err), }, - Err(err) => tracing::debug!("Error receiving pin from client: {:?}", err), + None => tracing::debug!("Pin channel closed before receiving pin from client."), } } UxUpdate::PresenceRequired => { @@ -439,7 +451,7 @@ async fn handle_usb_updates( enum UsbUvMessage { NeedsPin { attempts_left: Option, - pin_tx: oneshot::Sender, + pin_tx: mpsc::Sender, }, NeedsUserVerification { attempts_left: Option, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index a21f369b..a8ca96ae 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -1,12 +1,9 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::thread; +use std::sync::Arc; use std::time::Duration; use base64::Engine; use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD}; -use gettextrs::{gettext, LocaleCategory}; -use gtk::{gio, glib}; use libwebauthn::ops::webauthn::{ Assertion, CredentialProtectionExtension, GetAssertionHmacOrPrfInput, @@ -19,10 +16,7 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialUserEntity, }; use ring::digest; -use tokio::sync::{ - mpsc::{self, Receiver, Sender}, - Mutex as AsyncMutex, -}; +use tokio::sync::Mutex as AsyncMutex; use zbus::{ connection::{self, Connection}, fdo, interface, @@ -30,98 +24,41 @@ use zbus::{ Result, }; -use crate::application::ExampleApplication; -use crate::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; -use crate::credential_service::hybrid::InternalHybridHandler; -use crate::credential_service::usb::LocalUsbHandler; -use crate::credential_service::CredentialService; -use crate::view_model::CredentialType; -use crate::view_model::Operation; -use crate::view_model::{self, ViewEvent, ViewUpdate}; +use crate::credential_service::CredentialManagementClient; +use crate::gui::ViewRequest; +use crate::view_model::{CredentialType, Operation}; use crate::webauthn::{ self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, }; -pub(crate) async fn start_service(service_name: &str, path: &str) -> Result { - let (gui_tx, gui_rx) = mpsc::channel(1); - let lock: Arc>)>>> = +pub(crate) async fn start_service( + service_name: &str, + path: &str, + gui_tx: async_std::channel::Sender, + manager_client: C, +) -> Result { + let lock: Arc>> = Arc::new(AsyncMutex::new(gui_tx)); - start_gui_thread(gui_rx); connection::Builder::session()? .name(service_name)? - .serve_at(path, CredentialManager { app_lock: lock })? + .serve_at( + path, + CredentialManager { + app_lock: lock, + manager_client, + }, + )? .build() .await } -fn start_gui_thread(mut rx: Receiver<(CredentialRequest, Sender>)>) { - thread::Builder::new() - .name("gui".into()) - .spawn(move || { - while let Some((cred_request, response_tx)) = rx.blocking_recv() { - let (tx_update, rx_update) = async_std::channel::unbounded::(); - let (tx_event, rx_event) = async_std::channel::unbounded::(); - let data = Arc::new(Mutex::new(None)); - let operation = match &cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create { - cred_type: CredentialType::Passkey, - }, - CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get { - cred_types: vec![CredentialType::Passkey], - }, - }; - let credential_service = CredentialService::new( - cred_request, - data.clone(), - InternalHybridHandler::new(), - LocalUsbHandler {}, - ); - let event_loop = async_std::task::spawn(async move { - let mut vm = view_model::ViewModel::new( - operation, - credential_service, - rx_event, - tx_update, - ); - vm.start_event_loop().await; - println!("event loop ended?"); - }); - start_gtk_app(tx_event, rx_update); - - async_std::task::block_on(event_loop.cancel()); - let lock = data.lock().unwrap(); - let response = lock.as_ref().cloned(); - response_tx.blocking_send(response).unwrap(); - } - }) - .unwrap(); -} - -fn start_gtk_app( - tx_event: async_std::channel::Sender, - rx_update: async_std::channel::Receiver, -) { - // Prepare i18n - gettextrs::setlocale(LocaleCategory::LcAll, ""); - gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); - gettextrs::textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); - - if glib::application_name().is_none() { - glib::set_application_name(&gettext("Credential Manager")); - } - let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file"); - gio::resources_register(&res); - - let app = ExampleApplication::new(tx_event, rx_update); - app.run(); -} - -struct CredentialManager { - app_lock: Arc>)>>>, +struct CredentialManager { + app_lock: Arc>>, + manager_client: C, } #[interface(name = "xyz.iinuwa.credentials.CredentialManagerUi1")] -impl CredentialManager { +impl CredentialManager { async fn create_credential( &self, request: CreateCredentialRequest, @@ -144,13 +81,13 @@ impl CredentialManager { "Could not parse passkey creation request: {e:?}" )) })?; - let request = + let cred_request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); - let (data_tx, mut data_rx) = mpsc::channel(1); - tx.send((request, data_tx)).await.unwrap(); - if let Some(CredentialResponse::CreatePublicKeyCredentialResponse( - cred_response, - )) = data_rx.recv().await.unwrap() + + let response = execute_flow(&tx, &self.manager_client, &cred_request).await?; + + if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = + response { let public_key_response = CreatePublicKeyCredentialResponse::try_from_ctap2_response( @@ -191,6 +128,8 @@ impl CredentialManager { "Cross-origin public-key credentials are not allowed.", ))); } + // Setup request + // TODO: assert that RP ID is bound to origin: // - if RP ID is not set, set the RP ID to the origin's effective domain // - if RP ID is set, assert that it matches origin's effective domain @@ -203,14 +142,13 @@ impl CredentialManager { "Could not parse passkey assertion request.".to_owned(), ) })?; - let request = + let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); - let (data_tx, mut data_rx) = mpsc::channel(1); - tx.send((request, data_tx)).await.unwrap(); - match data_rx.recv().await { - Some(Some(CredentialResponse::GetPublicKeyCredentialResponse( - cred_response, - ))) => { + + let response = execute_flow(&tx, &self.manager_client, &cred_request).await?; + + match response { + CredentialResponse::GetPublicKeyCredentialResponse(cred_response) => { let public_key_response = GetPublicKeyCredentialResponse::try_from_ctap2_response( &cred_response, @@ -218,10 +156,9 @@ impl CredentialManager { )?; Ok(public_key_response.into()) } - Some(_) => Err(fdo::Error::Failed( + _ => Err(fdo::Error::Failed( "Invalid credential response received from authenticator".to_string(), )), - None => Err(fdo::Error::Failed("User cancelled operation".to_string())), } } _ => Err(fdo::Error::Failed( @@ -252,6 +189,44 @@ impl CredentialManager { } } +async fn execute_flow( + gui_tx: &async_std::channel::Sender, + manager_client: &C, + cred_request: &CredentialRequest, +) -> Result { + manager_client + .init_request(cred_request.clone()) + .await + .map_err(|_| fdo::Error::Failed("Request already running".to_string()))?; + + // start GUI + let operation = match &cred_request { + CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create { + cred_type: CredentialType::Passkey, + }, + CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get { + cred_types: vec![CredentialType::Passkey], + }, + }; + let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); + let view_request = ViewRequest { + operation, + signal: signal_tx, + }; + gui_tx.send(view_request).await.unwrap(); + + // wait for gui to complete + signal_rx.await.map_err(|_| { + zbus::Error::Failure("GUI channel closed before completing request.".to_string()) + })?; + + // finish up + manager_client.complete_auth().await.map_err(|err| { + tracing::error!("Error retrieving credential: {:?}", err); + zbus::Error::Failure("Error retrieving credential".to_string()) + }) +} + // D-Bus <-> internal types #[derive(Clone, Debug)] pub(crate) enum CredentialRequest { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui.rs new file mode 100644 index 00000000..d53ca861 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui.rs @@ -0,0 +1,71 @@ +use std::thread; + +use async_std::channel::Receiver; +use gettextrs::{gettext, LocaleCategory}; +use gtk::{gio, glib}; +use tokio::sync::oneshot; + +use crate::application::ExampleApplication; +use crate::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; +use crate::{ + credential_service::CredentialServiceClient, + view_model::{self, Operation, ViewEvent, ViewUpdate}, +}; + +pub struct ViewRequest { + pub operation: Operation, + pub signal: oneshot::Sender<()>, +} + +pub(super) fn start_gui_thread( + rx: Receiver, + client: C, +) { + thread::Builder::new() + .name("gui".into()) + .spawn(move || { + // D-Bus received a request and needs a window open + while let Ok(view_request) = rx.recv_blocking() { + run_gui(client.clone(), view_request); + } + }) + .unwrap(); +} + +fn run_gui(client: C, request: ViewRequest) { + let ViewRequest { + operation, + signal: response_tx, + } = request; + let (tx_update, rx_update) = async_std::channel::unbounded::(); + let (tx_event, rx_event) = async_std::channel::unbounded::(); + let event_loop = async_std::task::spawn(async move { + let mut vm = view_model::ViewModel::new(operation, client, rx_event, tx_update); + vm.start_event_loop().await; + println!("event loop ended?"); + }); + + start_gtk_app(tx_event, rx_update); + + async_std::task::block_on(event_loop.cancel()); + response_tx.send(()).unwrap(); +} + +fn start_gtk_app( + tx_event: async_std::channel::Sender, + rx_update: async_std::channel::Receiver, +) { + // Prepare i18n + gettextrs::setlocale(LocaleCategory::LcAll, ""); + gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); + gettextrs::textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); + + if glib::application_name().is_none() { + glib::set_application_name(&gettext("Credential Manager")); + } + let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file"); + gio::resources_register(&res); + + let app = ExampleApplication::new(tx_event, rx_update); + app.run(); +} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index a89013be..9246fa72 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -5,13 +5,18 @@ mod config; mod cose; mod credential_service; mod dbus; +mod gui; mod serde; #[allow(dead_code)] mod view_model; mod webauthn; mod window; -use std::error::Error; +use std::{error::Error, sync::Arc}; + +use crate::credential_service::{ + hybrid::InternalHybridHandler, usb::LocalUsbHandler, CredentialService, InProcessServer, +}; #[tokio::main] async fn main() { @@ -26,10 +31,28 @@ async fn main() { } async fn run() -> Result<(), Box> { + let credential_service = + CredentialService::new(InternalHybridHandler::new(), LocalUsbHandler {}); + print!("Starting credential service...\t"); + let (mut cred_server, cred_mgr, cred_client) = InProcessServer::new(credential_service); + // let cred_server = Arc::new(Mutex::new(cred_server)); + tokio::spawn(async move { + cred_server.run().await; + }); + println!(" ✅"); + + // this one + print!("Starting GUI thread...\t"); + let (dbus_to_gui_tx, dbus_to_gui_rx) = async_std::channel::unbounded(); + gui::start_gui_thread(dbus_to_gui_rx, Arc::new(cred_client)); + println!(" ✅"); + + print!("Starting D-Bus service..."); let service_name = "xyz.iinuwa.credentials.CredentialManagerUi"; let path = "/xyz/iinuwa/credentials/CredentialManagerUi"; - let _conn = dbus::start_service(service_name, path).await?; - println!("Started"); + let _conn = dbus::start_service(service_name, path, dbus_to_gui_tx, cred_mgr).await?; + println!(" ✅"); + println!("Waiting for messages..."); loop { // wait forever, handle D-Bus in the background std::future::pending::<()>().await; 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 a6076c8d..476f7aa1 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 @@ -7,7 +7,7 @@ use async_std::{ channel::{Receiver, Sender}, sync::Mutex, }; -use tokio::sync::oneshot; +use tokio::sync::{mpsc, oneshot}; use tracing::{error, info}; use crate::credential_service::{CredentialServiceClient, UsbState}; @@ -32,7 +32,7 @@ where providers: Vec, - usb_pin_tx: Option>>, + usb_pin_tx: Option>>>, hybrid_qr_state: HybridState, hybrid_qr_code_data: Option>, @@ -162,12 +162,12 @@ impl ViewModel { let tx = self.bg_update.clone(); let mut stream = { let cred_service = cred_service.lock().await; - cred_service.get_usb_credential() + cred_service.get_usb_credential().await }; async_std::task::spawn(async move { // TODO: add cancellation while let Some(usb_state) = stream.next().await { - tx.send(BackgroundEvent::UsbStateChanged(usb_state.clone())) + tx.send(BackgroundEvent::UsbStateChanged(usb_state)) .await .unwrap(); /* @@ -202,7 +202,7 @@ impl ViewModel { Transport::HybridQr => { let tx = self.bg_update.clone(); let cred_service = self.credential_service.clone(); - let mut stream = cred_service.lock().await.get_hybrid_credential(); + let mut stream = cred_service.lock().await.get_hybrid_credential().await; async_std::task::spawn(async move { while let Some(state) = stream.next().await { let state = state.into(); @@ -263,15 +263,9 @@ impl ViewModel { println!("Selected device {id}"); } Event::View(ViewEvent::UsbPinEntered(pin)) => { - let pin_tx = self.usb_pin_tx.take().unwrap(); - match Arc::try_unwrap(pin_tx) { - Ok(pin_tx) => { - if let Err(_) = pin_tx.send(pin) { - error!("Failed to send pin to device"); - } - } - Err(e) => { - error!("Failed to send pin to device: {:?}", e); + if let Some(pin_tx) = self.usb_pin_tx.take() { + if let Err(_) = pin_tx.lock().await.send(pin).await { + error!("Failed to send pin to device"); } } } @@ -301,7 +295,7 @@ impl ViewModel { attempts_left, pin_tx, } => { - let _ = self.usb_pin_tx.insert(pin_tx); + let _ = self.usb_pin_tx.insert(Arc::new(Mutex::new(pin_tx))); self.tx_update .send(ViewUpdate::UsbNeedsPin { attempts_left }) .await @@ -320,7 +314,6 @@ impl ViewModel { .unwrap(); } UsbState::Completed => { - self.credential_service.lock().await.complete_auth(); self.tx_update.send(ViewUpdate::Completed).await.unwrap(); } UsbState::SelectingDevice => { @@ -356,7 +349,6 @@ impl ViewModel { } HybridState::Completed => { self.hybrid_qr_code_data = None; - self.credential_service.lock().await.complete_auth(); self.tx_update.send(ViewUpdate::Completed).await.unwrap(); } HybridState::UserCancelled => { From c48ce1a38c5923eefc08597041db39858df87c52 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 5 Jun 2025 11:57:07 -0500 Subject: [PATCH 10/13] Clean up some warnings in authenticator transport code --- .../src/credential_service/hybrid.rs | 270 +++++++++--------- .../src/credential_service/mod.rs | 22 +- .../src/credential_service/server.rs | 17 +- .../src/credential_service/usb.rs | 123 +++----- .../src/main.rs | 8 +- 5 files changed, 188 insertions(+), 252 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index b64de859..19a55646 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -1,27 +1,20 @@ use std::fmt::Debug; -use std::task::Poll; -use async_std::channel::Receiver; use async_stream::stream; -use futures_lite::{FutureExt, Stream}; -use libwebauthn::fido::{AuthenticatorData, AuthenticatorDataFlags}; -use libwebauthn::ops::webauthn::{Assertion, GetAssertionResponse}; -use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transport}; +use futures_lite::Stream; use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint}; use libwebauthn::transport::Device; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; -use tokio::sync::mpsc::error::TryRecvError; use crate::dbus::CredentialRequest; use super::AuthenticatorResponse; pub(crate) trait HybridHandler { - // type Stream: Stream; fn start( &self, request: &CredentialRequest, - ) -> impl Stream + Unpin + Send + Sized + 'static; + ) -> impl Stream + Unpin + Send + Sized + 'static; } #[derive(Debug)] @@ -33,12 +26,10 @@ impl InternalHybridHandler { } impl HybridHandler for InternalHybridHandler { - // type Stream = InternalHybridStream; - fn start( &self, request: &CredentialRequest, - ) -> impl Stream + Unpin + Send + Sized + 'static { + ) -> impl Stream + Unpin + Send + Sized + 'static { let request = request.clone(); let (tx, rx) = async_std::channel::unbounded(); tokio::spawn(async move { @@ -67,7 +58,7 @@ impl HybridHandler for InternalHybridHandler { let response: AuthenticatorResponse = loop { match &request { CredentialRequest::CreatePublicKeyCredentialRequest(make_request) => { - match channel.webauthn_make_credential(&make_request).await { + match channel.webauthn_make_credential(make_request).await { Ok(response) => break Ok(response.into()), Err(WebAuthnError::Ctap(ctap_error)) => { if ctap_error.is_retryable_user_error() { @@ -80,7 +71,7 @@ impl HybridHandler for InternalHybridHandler { }; } CredentialRequest::GetPublicKeyCredentialRequest(get_request) => { - match channel.webauthn_get_assertion(&get_request).await { + match channel.webauthn_get_assertion(get_request).await { Ok(response) => break Ok(response.into()), Err(WebAuthnError::Ctap(ctap_error)) => { if ctap_error.is_retryable_user_error() { @@ -102,156 +93,37 @@ impl HybridHandler for InternalHybridHandler { }); Box::pin(stream! { while let Ok(state) = rx.recv().await { - yield state + yield HybridEvent { state } } }) - // InternalHybridStream { rx } - } -} - -/* -pub struct InternalHybridStream { - rx: Receiver, -} - -impl Stream for InternalHybridStream { - type Item = HybridStateInternal; - - fn poll_next( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - match self.rx.poll() { - Ok(state) => Poll::Ready(state), - Err(_) => Poll:Ready(None), - /* - Poll::Pending => Poll::Pending, - Poll::Ready(Ok(state)) => Poll::Ready(Some(state)), - Poll::Ready(Err(_)) => Poll::Ready(None), - */ - } - } -} - -*/ -#[derive(Debug)] -pub struct DummyHybridHandler { - stream: DummyHybridStateStream, -} - -impl DummyHybridHandler { - #[cfg(test)] - pub fn new(states: Vec) -> Self { - Self { - stream: DummyHybridStateStream { states }, - } - } -} - -impl Default for DummyHybridHandler { - fn default() -> Self { - Self { - stream: DummyHybridStateStream::default(), - } - } -} -impl HybridHandler for DummyHybridHandler { - // type Stream = DummyHybridStateStream; - - fn start( - &self, - _request: &CredentialRequest, - ) -> impl Stream + Send + Sized + Unpin + 'static { - self.stream.clone() } } #[derive(Clone, Debug)] -pub struct DummyHybridStateStream { - states: Vec, -} - -impl Default for DummyHybridStateStream { - fn default() -> Self { - let qr_code = String::from("FIDO:/078241338926040702789239694720083010994762289662861130514766991835876383562063181103169246410435938367110394959927031730060360967994421343201235185697538107096654083332"); - // SHA256("webauthn.io") - let rp_id_hash = [ - 0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20, - 0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0xb, - 0x60, 0x84, 0x1e, 0xf0, - ]; - - let auth_data = AuthenticatorData { - rp_id_hash, - flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::USER_VERIFIED, - signature_count: 1, - attested_credential: None, - extensions: None, - }; - - let assertion = Assertion { - credential_id: Some(Ctap2PublicKeyCredentialDescriptor { - id: vec![0xca, 0xb1, 0xe].into(), - r#type: libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialType::PublicKey, - transports: Some(vec![Ctap2Transport::Hybrid]), - }), - authenticator_data: auth_data, - signature: Vec::new(), - user: None, - credentials_count: Some(1), - user_selected: None, - large_blob_key: None, - unsigned_extensions_output: None, - enterprise_attestation: None, - attestation_statement: None, - }; - let response = GetAssertionResponse { - assertions: vec![assertion], - }; - DummyHybridStateStream { - states: vec![ - HybridStateInternal::Init(qr_code), - HybridStateInternal::Waiting, - HybridStateInternal::Connecting, - HybridStateInternal::Completed(response.into()), - ], - } - } -} - -impl Stream for DummyHybridStateStream { - type Item = HybridStateInternal; - - fn poll_next( - self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> Poll> { - if self.states.len() == 0 { - Poll::Ready(None) - } else { - Poll::Ready(Some((self.get_mut()).states.remove(0))) - } - } -} - -#[derive(Clone, Debug)] -pub enum HybridStateInternal { +pub(super) enum HybridStateInternal { /// The FIDO string to be displayed to the user, which contains QR secret /// and public key. Init(String), /// Awaiting BLE advert from phone. Waiting, + /// BLE advertisement has been received from phone, tunnel is being established Connecting, /// Authenticator data Completed(AuthenticatorResponse), + // TODO(cancellation) // This isn't actually sent from the server. + #[allow(dead_code)] UserCancelled, } +pub struct HybridEvent { + pub(super) state: HybridStateInternal, +} + #[derive(Clone, Debug)] pub enum HybridState { /// The FIDO string to be displayed to the user, which contains QR secret @@ -281,3 +153,117 @@ impl From for HybridState { } } } + +#[cfg(test)] +pub(super) mod test { + use std::task::Poll; + + use futures_lite::Stream; + use libwebauthn::{ + fido::{AuthenticatorData, AuthenticatorDataFlags}, + ops::webauthn::{Assertion, GetAssertionResponse}, + proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transport}, + }; + + use crate::dbus::CredentialRequest; + + use super::{HybridEvent, HybridHandler, HybridStateInternal}; + #[derive(Debug)] + pub struct DummyHybridHandler { + stream: DummyHybridStateStream, + } + + impl DummyHybridHandler { + #[cfg(test)] + pub fn new(states: Vec) -> Self { + Self { + stream: DummyHybridStateStream { states }, + } + } + } + + impl Default for DummyHybridHandler { + fn default() -> Self { + Self { + stream: DummyHybridStateStream::default(), + } + } + } + impl HybridHandler for DummyHybridHandler { + fn start( + &self, + _request: &CredentialRequest, + ) -> impl Stream + Send + Sized + Unpin + 'static { + self.stream.clone() + } + } + + #[derive(Clone, Debug)] + pub struct DummyHybridStateStream { + states: Vec, + } + + impl Default for DummyHybridStateStream { + fn default() -> Self { + let qr_code = String::from("FIDO:/078241338926040702789239694720083010994762289662861130514766991835876383562063181103169246410435938367110394959927031730060360967994421343201235185697538107096654083332"); + // SHA256("webauthn.io") + let rp_id_hash = [ + 0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20, + 0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0xb, + 0x60, 0x84, 0x1e, 0xf0, + ]; + + let auth_data = AuthenticatorData { + rp_id_hash, + flags: AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::USER_VERIFIED, + signature_count: 1, + attested_credential: None, + extensions: None, + }; + + let assertion = Assertion { + credential_id: Some(Ctap2PublicKeyCredentialDescriptor { + id: vec![0xca, 0xb1, 0xe].into(), + r#type: libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialType::PublicKey, + transports: Some(vec![Ctap2Transport::Hybrid]), + }), + authenticator_data: auth_data, + signature: Vec::new(), + user: None, + credentials_count: Some(1), + user_selected: None, + large_blob_key: None, + unsigned_extensions_output: None, + enterprise_attestation: None, + attestation_statement: None, + }; + let response = GetAssertionResponse { + assertions: vec![assertion], + }; + DummyHybridStateStream { + states: vec![ + HybridStateInternal::Init(qr_code), + HybridStateInternal::Waiting, + HybridStateInternal::Connecting, + HybridStateInternal::Completed(response.into()), + ], + } + } + } + + impl Stream for DummyHybridStateStream { + type Item = HybridEvent; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + if self.states.len() == 0 { + Poll::Ready(None) + } else { + let state = (self.get_mut()).states.remove(0); + Poll::Ready(Some(HybridEvent { state })) + } + } + } +} 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 cd09ea04..b15eea56 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 @@ -16,6 +16,7 @@ use libwebauthn::{ }; use crate::{ + credential_service::{hybrid::HybridEvent, usb::UsbEvent}, dbus::{ CredentialRequest, CredentialResponse, GetAssertionResponseInternal, MakeCredentialResponseInternal, @@ -118,7 +119,7 @@ pub struct HybridStateStream { impl Stream for HybridStateStream where - H: Stream + Unpin + Sized, + H: Stream + Unpin + Sized, { type Item = HybridState; @@ -129,10 +130,9 @@ where let cred_response = &self.cred_response.clone(); match Box::pin(Box::pin(self).as_mut().inner.next()).poll(cx) { Poll::Pending => Poll::Pending, - Poll::Ready(Some(state)) => { + Poll::Ready(Some(HybridEvent { state })) => { if let HybridStateInternal::Completed(hybrid_response) = &state { - let response = - hybrid_response.into_cred_response(&["hybrid"], "cross-platform"); + let response = hybrid_response.as_cred_response(&["hybrid"], "cross-platform"); let mut cred_response = cred_response.lock().unwrap(); cred_response.replace(response); } @@ -150,7 +150,7 @@ struct UsbStateStream { impl Stream for UsbStateStream where - H: Stream + Unpin + Sized, + H: Stream + Unpin + Sized, { type Item = UsbState; @@ -161,9 +161,9 @@ where let cred_response = &self.cred_response.clone(); match Box::pin(Box::pin(self).as_mut().inner.next()).poll(cx) { Poll::Pending => Poll::Pending, - Poll::Ready(Some(state)) => { + Poll::Ready(Some(UsbEvent { state })) => { if let UsbStateInternal::Completed(response) = &state { - let response = response.into_cred_response(&["usb"], "cross-platform"); + let response = response.as_cred_response(&["usb"], "cross-platform"); let mut cred_response = cred_response.lock().unwrap(); cred_response.replace(response); } @@ -180,7 +180,7 @@ enum AuthenticatorResponse { CredentialsAsserted(GetAssertionResponse), } impl AuthenticatorResponse { - fn into_cred_response(&self, transports: &[&str], modality: &str) -> CredentialResponse { + fn as_cred_response(&self, transports: &[&str], modality: &str) -> CredentialResponse { match self { AuthenticatorResponse::CredentialCreated(make_response) => { CredentialResponse::CreatePublicKeyCredentialResponse( @@ -225,12 +225,12 @@ mod test { use async_std::stream::StreamExt; use crate::{ - credential_service::usb::LocalUsbHandler, + credential_service::usb::InProcessUsbHandler, dbus::{CreateCredentialRequest, CreatePublicKeyCredentialRequest, CredentialRequest}, }; use super::{ - hybrid::{DummyHybridHandler, HybridStateInternal}, + hybrid::{test::DummyHybridHandler, HybridStateInternal}, AuthenticatorResponse, CredentialService, }; @@ -246,7 +246,7 @@ mod test { HybridStateInternal::Connecting, HybridStateInternal::Completed(authenticator_response), ]); - let usb_handler = LocalUsbHandler {}; + let usb_handler = InProcessUsbHandler {}; let cred_service = Arc::new(CredentialService::new(hybrid_handler, usb_handler)); cred_service.init_request(&request).unwrap(); let mut stream = cred_service.get_hybrid_credential(); diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs index 7391cc1c..69bd90c0 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs @@ -20,7 +20,7 @@ pub enum ServiceRequest { } enum ManagementRequest { - InitRequest(CredentialRequest), + InitRequest(Box), CompleteAuth, } @@ -30,6 +30,9 @@ enum ManagementResponse { CompleteAuth(Option), } +// Clippy complains that these variant names have the same prefix, but that's +// intentional for now. +#[allow(clippy::enum_variant_names)] pub enum ServiceResponse { GetDevices(Vec), GetHybridCredential(Pin + Send>>), @@ -115,7 +118,7 @@ impl InProcessManager { impl CredentialManagementClient for InProcessManager { async fn init_request(&self, cred_request: CredentialRequest) -> Result<(), String> { let response = self - .send(ManagementRequest::InitRequest(cred_request)) + .send(ManagementRequest::InitRequest(Box::new(cred_request))) .await .unwrap(); if let ManagementResponse::InitRequest(result) = response { @@ -197,19 +200,19 @@ impl CredentialServiceClient for InProcessClient { impl CredentialServiceClient for Arc { fn get_available_public_key_devices(&self) -> impl Future, ()>> { - InProcessClient::get_available_public_key_devices(&self) + InProcessClient::get_available_public_key_devices(self) } fn get_hybrid_credential( &self, ) -> impl Future + Send>>> { - InProcessClient::get_hybrid_credential(&self) + InProcessClient::get_hybrid_credential(self) } fn get_usb_credential( &self, ) -> impl Future + Send>>> { - InProcessClient::get_usb_credential(&self) + InProcessClient::get_usb_credential(self) } } @@ -217,9 +220,7 @@ impl CredentialServiceClient for Arc { pub struct InProcessServer where H: HybridHandler + Debug, - // ::Stream: Unpin + Send + Sized + 'static, U: UsbHandler + Debug, - // ::Stream: Unpin + Send + Sized + 'static, { svc: CredentialService, rx: mpsc::Receiver<( @@ -231,9 +232,7 @@ where impl InProcessServer where H: HybridHandler + Debug, - // ::Stream: Unpin + Send + Sized + 'static, U: UsbHandler + Debug, - // ::Stream: Unpin + Send + Sized + 'static, { pub fn new(svc: CredentialService) -> (Self, InProcessManager, InProcessClient) { let (tx, rx) = mpsc::channel(256); diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs index db0d24ef..6c8685f6 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs @@ -1,4 +1,4 @@ -use std::{ops::DerefMut, time::Duration}; +use std::time::Duration; use async_stream::stream; use futures_lite::Stream; @@ -15,18 +15,16 @@ use crate::dbus::CredentialRequest; use super::AuthenticatorResponse; pub(crate) trait UsbHandler { - // type Stream: Stream; - // fn start(&self, request: &CredentialRequest) -> Self::Stream; fn start( &self, request: &CredentialRequest, - ) -> impl Stream + Send + Sized + Unpin + 'static; + ) -> impl Stream + Send + Sized + Unpin + 'static; } #[derive(Debug)] -pub struct LocalUsbHandler {} +pub struct InProcessUsbHandler {} -impl LocalUsbHandler { +impl InProcessUsbHandler { async fn process( tx: Sender, cred_request: CredentialRequest, @@ -103,12 +101,10 @@ impl LocalUsbHandler { pin_tx, })) => Ok(UsbStateInternal::NeedsPin { attempts_left, - pin_tx: pin_tx, + pin_tx, }), Some(Ok(UsbUvMessage::NeedsUserVerification { attempts_left })) => { - Ok(UsbStateInternal::NeedsUserVerification { - attempts_left: attempts_left, - }) + Ok(UsbStateInternal::NeedsUserVerification { attempts_left }) } Some(Ok(UsbUvMessage::NeedsUserPresence)) => { Ok(UsbStateInternal::NeedsUserPresence) @@ -132,29 +128,22 @@ impl LocalUsbHandler { UsbStateInternal::NeedsPin { .. } | UsbStateInternal::NeedsUserVerification { .. } | UsbStateInternal::NeedsUserPresence => match signal_rx.recv().await { - Some(msg) => { - let state = match msg? { - UsbUvMessage::NeedsPin { - attempts_left, - pin_tx, - } => Ok(UsbStateInternal::NeedsPin { - attempts_left, - pin_tx: pin_tx, - }), - UsbUvMessage::NeedsUserVerification { attempts_left } => { - Ok(UsbStateInternal::NeedsUserVerification { - attempts_left: attempts_left, - }) - } - UsbUvMessage::NeedsUserPresence => { - Ok(UsbStateInternal::NeedsUserPresence) - } - UsbUvMessage::ReceivedCredential(response) => { - Ok(UsbStateInternal::Completed(response.clone())) - } - }; - state - } + Some(msg) => match msg? { + UsbUvMessage::NeedsPin { + attempts_left, + pin_tx, + } => Ok(UsbStateInternal::NeedsPin { + attempts_left, + pin_tx, + }), + UsbUvMessage::NeedsUserVerification { attempts_left } => { + Ok(UsbStateInternal::NeedsUserVerification { attempts_left }) + } + UsbUvMessage::NeedsUserPresence => Ok(UsbStateInternal::NeedsUserPresence), + UsbUvMessage::ReceivedCredential(response) => { + Ok(UsbStateInternal::Completed(response.clone())) + } + }, None => Err("USB UV handler channel closed".to_string()), }, UsbStateInternal::Completed(_) => Ok(prev_usb_state), @@ -163,35 +152,11 @@ impl LocalUsbHandler { tx.send(state.clone()) .await .map_err(|_| "Receiver channel closed".to_string())?; - match state { - UsbStateInternal::Completed(_) => break Ok(()), - _ => {} + if let UsbStateInternal::Completed(_) = state { + break Ok(()); } } } - - /* - async fn cancel_device_discovery_usb(&mut self) -> Result<(), String> { - *self.usb_state.lock().await = UsbState::Idle; - println!("frontend: Cancel USB request"); - Ok(()) - } - */ - - /* - async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> { - let current_state = self.usb_state.lock().await.clone(); - match current_state { - UsbState::NeedsPin { - attempts_left: Some(attempts_left), - } if attempts_left > 1 => { - self.usb_uv_handler.send_pin(pin).await; - Ok(()) - } - _ => Err(()), - } - } - */ } async fn handle_events( @@ -207,10 +172,10 @@ async fn handle_events( }); match cred_request { CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request) => loop { - match channel.webauthn_make_credential(&make_cred_request).await { + match channel.webauthn_make_credential(make_cred_request).await { Ok(response) => { notify_ceremony_completed( - &signal_tx, + signal_tx, AuthenticatorResponse::CredentialCreated(response), ) .await; @@ -221,16 +186,16 @@ async fn handle_events( continue; } Err(err) => { - notify_ceremony_failed(&signal_tx, err.to_string()).await; + notify_ceremony_failed(signal_tx, err.to_string()).await; break; } }; }, CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request) => loop { - match channel.webauthn_get_assertion(&get_cred_request).await { + match channel.webauthn_get_assertion(get_cred_request).await { Ok(response) => { notify_ceremony_completed( - &signal_tx, + signal_tx, AuthenticatorResponse::CredentialsAsserted(response), ) .await; @@ -241,7 +206,7 @@ async fn handle_events( continue; } Err(err) => { - notify_ceremony_failed(&signal_tx, err.to_string()).await; + notify_ceremony_failed(signal_tx, err.to_string()).await; break; } }; @@ -263,46 +228,32 @@ async fn notify_ceremony_failed(signal_tx: &Sender> signal_tx.send(Err(err)).await.unwrap(); } -impl UsbHandler for LocalUsbHandler { - // type Stream = LocalUsbStateStream; - +impl UsbHandler for InProcessUsbHandler { fn start( &self, request: &CredentialRequest, - ) -> impl Stream + Send + Sized + Unpin + 'static { + ) -> impl Stream + Send + Sized + Unpin + 'static { let request = request.clone(); let (tx, mut rx) = mpsc::channel(32); tokio::spawn(async move { - if let Err(err) = LocalUsbHandler::process(tx, request).await { + if let Err(err) = InProcessUsbHandler::process(tx, request).await { tracing::error!("Error getting credential from USB: {:?}", err); } }); Box::pin(stream! { while let Some(state) = rx.recv().await { - yield state + yield UsbEvent { state } } }) - // LocalUsbStateStream { rx } } } -pub struct LocalUsbStateStream { - rx: Receiver, -} - -impl Stream for LocalUsbStateStream { - type Item = UsbStateInternal; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.deref_mut().rx.poll_recv(cx) - } +pub struct UsbEvent { + pub(super) state: UsbStateInternal, } #[derive(Clone, Debug, Default)] -pub enum UsbStateInternal { +pub(super) enum UsbStateInternal { /// Not polling for FIDO USB device. #[default] Idle, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index 9246fa72..2a3c28f1 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -15,7 +15,7 @@ mod window; use std::{error::Error, sync::Arc}; use crate::credential_service::{ - hybrid::InternalHybridHandler, usb::LocalUsbHandler, CredentialService, InProcessServer, + hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, InProcessServer, }; #[tokio::main] @@ -32,17 +32,17 @@ async fn main() { async fn run() -> Result<(), Box> { let credential_service = - CredentialService::new(InternalHybridHandler::new(), LocalUsbHandler {}); + CredentialService::new(InternalHybridHandler::new(), InProcessUsbHandler {}); print!("Starting credential service...\t"); let (mut cred_server, cred_mgr, cred_client) = InProcessServer::new(credential_service); - // let cred_server = Arc::new(Mutex::new(cred_server)); tokio::spawn(async move { cred_server.run().await; }); println!(" ✅"); - // this one print!("Starting GUI thread...\t"); + // this allows the D-Bus service to signal to the GUI to draw a window for + // executing the credential flow. let (dbus_to_gui_tx, dbus_to_gui_rx) = async_std::channel::unbounded(); gui::start_gui_thread(dbus_to_gui_rx, Arc::new(cred_client)); println!(" ✅"); From be7c38297d025d64322b80f29c6c4afe0f105afa Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 5 Jun 2025 11:57:07 -0500 Subject: [PATCH 11/13] Apply some cargo clippy fixes --- .../src/credential_service/hybrid.rs | 3 +++ .../src/credential_service/server.rs | 1 + .../src/view_model/gtk/mod.rs | 3 ++- .../src/view_model/mod.rs | 10 +++++++--- .../src/webauthn.rs | 8 ++++---- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index 19a55646..e9c881f9 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -55,6 +55,9 @@ impl HybridHandler for InternalHybridHandler { panic!(); } }; + if let Err(err) = tx.send(HybridStateInternal::Connected).await { + tracing::error!("Failed to send caBLE update: {:?}", err) + } let response: AuthenticatorResponse = loop { match &request { CredentialRequest::CreatePublicKeyCredentialRequest(make_request) => { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs index 69bd90c0..4f87f92d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs @@ -13,6 +13,7 @@ use super::hybrid::{HybridHandler, HybridState}; use super::usb::{UsbHandler, UsbState}; use super::CredentialService; +#[allow(clippy::enum_variant_names)] pub enum ServiceRequest { GetDevices, GetHybridCredential, 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 80f87119..1b39b83e 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 @@ -284,7 +284,8 @@ impl ViewModel { todo!(); } } - self.set_selected_device(&device.into()); + let device_object: DeviceObject = device.into(); + self.set_selected_device(device_object); self.set_selected_credential(""); } 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 476f7aa1..b2a8e898 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 @@ -7,7 +7,7 @@ use async_std::{ channel::{Receiver, Sender}, sync::Mutex, }; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; use tracing::{error, info}; use crate::credential_service::{CredentialServiceClient, UsbState}; @@ -152,7 +152,11 @@ impl ViewModel { todo!(); } }; - self.selected_credential = None; + // Remove the attribute below when we implement cancellation for at least one transport. + #[allow(unreachable_code)] + { + self.selected_credential = None; + } } // start discovery for newly selected device @@ -264,7 +268,7 @@ impl ViewModel { } Event::View(ViewEvent::UsbPinEntered(pin)) => { if let Some(pin_tx) = self.usb_pin_tx.take() { - if let Err(_) = pin_tx.lock().await.send(pin).await { + if pin_tx.lock().await.send(pin).await.is_err() { error!("Failed to send pin to device"); } } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs index fd580771..d15a4fc0 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs @@ -254,10 +254,10 @@ impl TryFrom for Ctap2PublicKeyCredentialDescriptor { #[zvariant(signature = "dict")] /// https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection pub(crate) struct AuthenticatorSelectionCriteria { - /// https://www.w3.org/TR/webauthn-3/#enum-attachment - #[zvariant(rename = "authenticatorAttachment")] - pub authenticator_attachment: Option, - + // /// https://www.w3.org/TR/webauthn-3/#enum-attachment + // #[zvariant(rename = "authenticatorAttachment")] + // pub authenticator_attachment: Option, + // /// https://www.w3.org/TR/webauthn-3/#enum-residentKeyRequirement #[zvariant(rename = "residentKey")] pub resident_key: Option, From 89d91153b870cabfe21ae504fd27e36799648e6a Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 5 Jun 2025 11:57:07 -0500 Subject: [PATCH 12/13] Remove bogus Waiting hybrid state --- .../src/credential_service/hybrid.rs | 13 +++---------- .../src/credential_service/mod.rs | 1 - .../src/view_model/mod.rs | 12 +----------- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index e9c881f9..32462c1a 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -104,13 +104,10 @@ impl HybridHandler for InternalHybridHandler { #[derive(Clone, Debug)] pub(super) enum HybridStateInternal { - /// The FIDO string to be displayed to the user, which contains QR secret - /// and public key. + /// Awaiting BLE advert from phone. Content is the FIDO string to be + /// displayed to the user, which contains QR secret and public key. Init(String), - /// Awaiting BLE advert from phone. - Waiting, - /// BLE advertisement has been received from phone, tunnel is being established Connecting, @@ -129,12 +126,10 @@ pub struct HybridEvent { #[derive(Clone, Debug)] pub enum HybridState { - /// The FIDO string to be displayed to the user, which contains QR secret + /// Awaiting BLE advert from phone. Content is the FIDO string to be displayed to the user, which contains QR secret /// and public key. Init(String), - /// Awaiting BLE advert from phone. - Waiting, /// BLE advertisement has been received from phone, tunnel is being established Connecting, @@ -149,7 +144,6 @@ impl From for HybridState { fn from(value: HybridStateInternal) -> Self { match value { HybridStateInternal::Init(qr_code) => HybridState::Init(qr_code), - HybridStateInternal::Waiting => HybridState::Waiting, HybridStateInternal::Connecting => HybridState::Connecting, HybridStateInternal::Completed(_) => HybridState::Completed, HybridStateInternal::UserCancelled => HybridState::UserCancelled, @@ -246,7 +240,6 @@ pub(super) mod test { DummyHybridStateStream { states: vec![ HybridStateInternal::Init(qr_code), - HybridStateInternal::Waiting, HybridStateInternal::Connecting, HybridStateInternal::Completed(response.into()), ], 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 b15eea56..803453e9 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 @@ -242,7 +242,6 @@ mod test { let hybrid_handler = DummyHybridHandler::new(vec![ HybridStateInternal::Init(qr_code), - HybridStateInternal::Waiting, HybridStateInternal::Connecting, HybridStateInternal::Completed(authenticator_response), ]); 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 b2a8e898..00b53a19 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 @@ -217,11 +217,6 @@ impl ViewModel { .await .unwrap(); } - HybridState::Waiting => { - tx.send(BackgroundEvent::HybridQrStateChanged(state)) - .await - .unwrap(); - } HybridState::Connecting => { tx.send(BackgroundEvent::HybridQrStateChanged(state)) .await @@ -343,7 +338,6 @@ impl ViewModel { .await .unwrap(); } - HybridState::Waiting => {} HybridState::Connecting => { self.hybrid_qr_code_data = None; self.tx_update @@ -431,12 +425,9 @@ pub enum HybridState { #[default] Idle, - /// QR code flow is starting + /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. Started(String), - /// QR code is being displayed, awaiting QR code scan and BLE advert from phone. - Waiting, - /// BLE advert received, connecting to caBLE tunnel with shared secret. Connecting, @@ -457,7 +448,6 @@ impl From for HybridState { crate::credential_service::hybrid::HybridState::Init(qr_code) => { HybridState::Started(qr_code) } - crate::credential_service::hybrid::HybridState::Waiting => HybridState::Waiting, crate::credential_service::hybrid::HybridState::Connecting => HybridState::Connecting, crate::credential_service::hybrid::HybridState::Completed => HybridState::Completed, crate::credential_service::hybrid::HybridState::UserCancelled => { From 4dee34413e00318d9972a722d3c74d2dd529c337 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 5 Jun 2025 11:57:07 -0500 Subject: [PATCH 13/13] Emit Connected state when hybrid tunnel is established --- .../src/credential_service/hybrid.rs | 7 +++++++ .../src/view_model/gtk/mod.rs | 10 ++++++++- .../src/view_model/mod.rs | 21 +++++++++++++++---- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs index 32462c1a..137f1ac5 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs @@ -111,6 +111,9 @@ pub(super) enum HybridStateInternal { /// BLE advertisement has been received from phone, tunnel is being established Connecting, + /// Hybrid tunnel has been established + Connected, + /// Authenticator data Completed(AuthenticatorResponse), @@ -133,6 +136,9 @@ pub enum HybridState { /// BLE advertisement has been received from phone, tunnel is being established Connecting, + /// Tunnel is established, waiting for user to release credential on their device. + Connected, + /// Authenticator data Completed, @@ -145,6 +151,7 @@ impl From for HybridState { match value { HybridStateInternal::Init(qr_code) => HybridState::Init(qr_code), HybridStateInternal::Connecting => HybridState::Connecting, + HybridStateInternal::Connected => HybridState::Connected, HybridStateInternal::Completed(_) => HybridState::Completed, HybridStateInternal::UserCancelled => HybridState::UserCancelled, } 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 1b39b83e..8e3555fa 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 @@ -162,10 +162,18 @@ impl ViewModel { view_model.set_qr_code_visible(false); _ = view_model.qr_code_paintable().take(); view_model.set_prompt( - "Device connected. Follow the instructions on your device", + "Connecting to your device. Make sure both devices are near each other and have Bluetooth enabled.", ); view_model.set_qr_spinner_visible(true); } + ViewUpdate::HybridConnected => { + view_model.set_qr_code_visible(false); + _ = view_model.qr_code_paintable().take(); + view_model.set_prompt( + "Device connected. Follow the instructions on your device", + ); + view_model.set_qr_spinner_visible(false); + } ViewUpdate::Completed => { view_model.set_qr_spinner_visible(false); view_model.set_completed(true); 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 00b53a19..7ce38ff4 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 @@ -222,6 +222,11 @@ impl ViewModel { .await .unwrap(); } + HybridState::Connected => { + tx.send(BackgroundEvent::HybridQrStateChanged(state)) + .await + .unwrap(); + } HybridState::Completed => { tx.send(BackgroundEvent::HybridQrStateChanged(state)) .await @@ -345,6 +350,13 @@ impl ViewModel { .await .unwrap(); } + HybridState::Connected => { + self.hybrid_qr_code_data = None; + self.tx_update + .send(ViewUpdate::HybridConnected) + .await + .unwrap(); + } HybridState::Completed => { self.hybrid_qr_code_data = None; self.tx_update.send(ViewUpdate::Completed).await.unwrap(); @@ -381,6 +393,7 @@ pub enum ViewUpdate { HybridNeedsQrCode(String), HybridConnecting, + HybridConnected, } pub enum BackgroundEvent { @@ -431,10 +444,9 @@ pub enum HybridState { /// BLE advert received, connecting to caBLE tunnel with shared secret. Connecting, - /* I don't think is necessary to signal. - /// Connected to device via caBLE tunnel. - Connected, - */ + /// Connected to device via caBLE tunnel. + Connected, + /// Credential received over tunnel. Completed, @@ -449,6 +461,7 @@ impl From for HybridState { HybridState::Started(qr_code) } crate::credential_service::hybrid::HybridState::Connecting => HybridState::Connecting, + crate::credential_service::hybrid::HybridState::Connected => HybridState::Connected, crate::credential_service::hybrid::HybridState::Completed => HybridState::Completed, crate::credential_service::hybrid::HybridState::UserCancelled => { HybridState::UserCancelled