From bd6aadee02a2de3ef9b4c6b6980dd3f211485a3e Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 6 Aug 2025 14:04:30 -0500 Subject: [PATCH 1/3] Re-enable debug logs for UI. Accidentally disabled when the project was renamed. --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 18f39854..e6743d42 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,7 +25,7 @@ "args": [], "env": { "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/credentialsd-ui/data", - "RUST_LOG": "creds_ui=debug,zbus::trace,zbus::object_server::debug" + "RUST_LOG": "credentialsd_ui=debug,zbus::trace,zbus::object_server::debug" }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", From 7eaacd8efb062e8e9386551c78f86901cebb4c02 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 6 Aug 2025 14:04:30 -0500 Subject: [PATCH 2/3] Begin work on user cancellation --- credentialsd-common/src/client.rs | 6 +- credentialsd-common/src/model.rs | 1 + credentialsd-common/src/server.rs | 5 ++ credentialsd-ui/src/client.rs | 15 +++- credentialsd-ui/src/dbus.rs | 10 +-- credentialsd-ui/src/gui/mod.rs | 11 ++- .../src/gui/view_model/gtk/application.rs | 16 ++++ credentialsd-ui/src/gui/view_model/gtk/mod.rs | 17 ++++ .../src/gui/view_model/gtk/window.rs | 13 +++ credentialsd-ui/src/gui/view_model/mod.rs | 11 ++- credentialsd/Cargo.lock | 7 +- credentialsd/Cargo.toml | 1 + credentialsd/src/credential_service/mod.rs | 80 ++++++++++++++----- credentialsd/src/dbus/flow_control.rs | 24 ++++-- credentialsd/src/dbus/ui_control.rs | 5 +- 15 files changed, 180 insertions(+), 42 deletions(-) diff --git a/credentialsd-common/src/client.rs b/credentialsd-common/src/client.rs index d89c6da3..f41a6ed3 100644 --- a/credentialsd-common/src/client.rs +++ b/credentialsd-common/src/client.rs @@ -2,7 +2,10 @@ use std::pin::Pin; use futures_lite::Stream; -use crate::model::{BackgroundEvent, Device}; +use crate::{ + model::{BackgroundEvent, Device}, + server::RequestId, +}; /// Used for communication from trusted UI to credential service pub trait FlowController { @@ -22,4 +25,5 @@ pub trait FlowController { &self, credential_id: String, ) -> impl Future> + Send; + fn cancel_request(&self, request_id: RequestId) -> impl Future> + Send; } diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index d968b63b..758d87da 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -188,6 +188,7 @@ pub enum ViewUpdate { HybridConnected, Completed, + Cancelled, Failed(String), } diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index 2e5b1c7a..d549b6f3 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -33,6 +33,7 @@ impl TryFrom for crate::model::BackgroundEvent { } } +// TODO: this can be changed to infallible From impl TryFrom for BackgroundEvent { type Error = zvariant::Error; fn try_from(value: crate::model::BackgroundEvent) -> Result { @@ -327,6 +328,9 @@ impl From for Value<'_> { } } +/// Identifier for a request to be used for cancellation. +pub type RequestId = u32; + #[derive(Serialize, Deserialize, Type)] pub enum ServiceError { /// Some unknown error with the authenticator occurred. @@ -625,6 +629,7 @@ impl From for Value<'_> { #[derive(Serialize, Deserialize, Type)] pub struct ViewRequest { pub operation: Operation, + pub id: RequestId, } fn value_to_owned(value: &Value<'_>) -> OwnedValue { diff --git a/credentialsd-ui/src/client.rs b/credentialsd-ui/src/client.rs index 7b46be99..22db41dd 100644 --- a/credentialsd-ui/src/client.rs +++ b/credentialsd-ui/src/client.rs @@ -1,5 +1,5 @@ use async_std::stream::Stream; -use credentialsd_common::client::FlowController; +use credentialsd_common::{client::FlowController, server::RequestId}; use futures_lite::StreamExt; use zbus::{Connection, zvariant}; @@ -101,4 +101,17 @@ impl FlowController for DbusCredentialClient { .await .map_err(|err| tracing::error!("Failed to select credential: {err}")) } + + async fn cancel_request(&self, request_id: RequestId) -> Result<(), ()> { + if self + .proxy() + .await? + .cancel_request(request_id) + .await + .is_err() + { + tracing::warn!("Failed to cancel request {request_id}"); + } + Ok(()) + } } diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index b281d8a8..87f5575e 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -1,5 +1,5 @@ use async_std::channel::Sender; -use credentialsd_common::server::{BackgroundEvent, Device, ViewRequest}; +use credentialsd_common::server::{BackgroundEvent, Device, RequestId, ViewRequest}; use zbus::{fdo, interface, proxy}; #[proxy( @@ -20,22 +20,20 @@ pub trait FlowControlService { async fn select_device(&self, device_id: String) -> fdo::Result<()>; async fn enter_client_pin(&self, pin: String) -> fdo::Result<()>; async fn select_credential(&self, credential_id: String) -> fdo::Result<()>; + async fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()>; #[zbus(signal)] async fn state_changed(update: BackgroundEvent) -> zbus::Result<()>; } pub struct UiControlService { - pub request_tx: Sender, + pub request_tx: Sender, } /// These methods are called by the credential service to control the UI. #[interface(name = "xyz.iinuwa.credentialsd.UiControl1")] impl UiControlService { - async fn launch_ui( - &self, - request: credentialsd_common::server::ViewRequest, - ) -> fdo::Result<()> { + async fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()> { tracing::debug!("Received UI launch request"); self.request_tx .send(request) diff --git a/credentialsd-ui/src/gui/mod.rs b/credentialsd-ui/src/gui/mod.rs index 02e72e8b..08ba8976 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -31,9 +31,16 @@ fn run_gui( 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, flow_controller, rx_event, tx_update); + let mut vm = + view_model::ViewModel::new(operation, flow_controller.clone(), rx_event, tx_update); vm.start_event_loop().await; - println!("event loop ended?"); + tracing::debug!("Finishing user request."); + // If cancellation fails, that's fine. + let _ = flow_controller + .lock() + .await + .cancel_request(request.id) + .await; }); view_model::gtk::start_gtk_app(tx_event, rx_update); diff --git a/credentialsd-ui/src/gui/view_model/gtk/application.rs b/credentialsd-ui/src/gui/view_model/gtk/application.rs index 629bbd64..ded3c493 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/application.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/application.rs @@ -11,6 +11,8 @@ use crate::config::{APP_ID, PKGDATADIR, PROFILE, VERSION}; use crate::gui::view_model::{ViewEvent, ViewUpdate}; mod imp { + use crate::gui::view_model::gtk::ModelState; + use super::*; use glib::{WeakRef, clone}; use std::{ @@ -67,6 +69,20 @@ mod imp { )); } }); + let window3 = window.clone(); + // TODO: merge these state callbacks into a single function + vm2.clone().connect_state_notify(move |vm| { + if let ModelState::Cancelled = vm.state() { + glib::spawn_future_local(clone!( + #[weak] + window3, + async move { + gtk::prelude::WidgetExt::activate_action(&window3, "window.close", None) + .unwrap() + } + )); + } + }); self.window .set(window.downgrade()) .expect("Window already set."); diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index 1d4be7d6..a1d5b1b2 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -51,6 +51,9 @@ mod imp { #[property(get, set)] pub prompt: RefCell, + #[property(get, set, builder(ModelState::Pending))] + pub state: RefCell, + #[property(get, set)] pub completed: RefCell, @@ -189,10 +192,14 @@ impl ViewModel { view_model.set_failed(true); view_model.set_prompt(error_msg); } + ViewUpdate::Cancelled => { + view_model.set_state(ModelState::Cancelled) + } } } Err(e) => { debug!("ViewModel event listener interrupted: {}", e); + view_model.set_state(ModelState::Cancelled); break; } } @@ -361,3 +368,13 @@ pub fn start_gtk_app( let app = ExampleApplication::new(tx_event, rx_update); app.run(); } + +#[derive(Clone, Copy, Debug, Default, glib::Enum)] +#[enum_type(name = "ModelState")] +pub enum ModelState { + #[default] + Pending, + Completed, + Failed, + Cancelled, +} diff --git a/credentialsd-ui/src/gui/view_model/gtk/window.rs b/credentialsd-ui/src/gui/view_model/gtk/window.rs index 12524f02..ab6de9bc 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/window.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/window.rs @@ -17,6 +17,8 @@ use crate::gui::view_model::Transport; mod imp { use gtk::Picture; + use crate::gui::view_model::ViewEvent; + use super::*; #[derive(Debug, Properties, gtk::CompositeTemplate)] @@ -106,6 +108,17 @@ mod imp { impl WindowImpl for ExampleApplicationWindow { // Save window state on delete event fn close_request(&self) -> glib::Propagation { + if let Some(vm) = self.view_model.borrow().as_ref() { + if vm + .get_sender() + .send_blocking(ViewEvent::UserCancelled) + .is_err() + { + tracing::warn!( + "Failed to notify the backend service that the user cancelled the request." + ); + }; + } if let Err(err) = self.obj().save_window_size() { tracing::warn!("Failed to save window state, {}", &err); } diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index a76a61b0..b9be3dd9 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -174,6 +174,9 @@ impl ViewModel { .unwrap(); } } + Event::View(ViewEvent::UserCancelled) => { + break; + } Event::Background(BackgroundEvent::UsbStateChanged(state)) => { match state { @@ -269,13 +272,18 @@ impl ViewModel { } HybridState::UserCancelled => { self.hybrid_qr_code_data = None; + break; } HybridState::Failed => { self.hybrid_qr_code_data = None; self.tx_update.send(ViewUpdate::Failed(String::from("Something went wrong. Try again later or use a different authenticator."))).await.unwrap(); } }; - } + } /* + Event::Background(BackgroundEvent::RequestCancelled(request_id)) => { + break; + } + */ }; } } @@ -287,6 +295,7 @@ pub enum ViewEvent { DeviceSelected(String), CredentialSelected(String), UsbPinEntered(String), + UserCancelled, } pub enum Event { diff --git a/credentialsd/Cargo.lock b/credentialsd/Cargo.lock index 249d4b27..4719972e 100644 --- a/credentialsd/Cargo.lock +++ b/credentialsd/Cargo.lock @@ -578,6 +578,7 @@ dependencies = [ "gio", "libwebauthn", "openssl", + "rand 0.9.2", "ring", "rustls", "serde", @@ -2098,9 +2099,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2966,7 +2967,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.1", + "rand 0.9.2", "rustls", "rustls-pki-types", "sha1", diff --git a/credentialsd/Cargo.toml b/credentialsd/Cargo.toml index 022ae0d9..0b0fa5f8 100644 --- a/credentialsd/Cargo.toml +++ b/credentialsd/Cargo.toml @@ -16,6 +16,7 @@ credentialsd-common = { path = "../credentialsd-common" } futures-lite = "2.6.0" libwebauthn = "~0.2.2" openssl = "0.10.72" +rand = "0.9.2" ring = "0.17.14" rustls = { version = "0.23.27", default-features = false, features = ["std", "tls12", "ring", "log", "logging", "prefer-post-quantum"] } serde = { version = "1.0.219", features = ["derive"] } diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index ca0b03df..6bc8a035 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -22,7 +22,7 @@ use credentialsd_common::{ CredentialRequest, CredentialResponse, Device, Error as CredentialServiceError, Operation, Transport, }, - server::ViewRequest, + server::{RequestId, ViewRequest}, }; use crate::credential_service::{hybrid::HybridEvent, usb::UsbEvent}; @@ -42,10 +42,22 @@ pub trait UiController { ) -> impl Future>> + Send; } -type RequestContext = ( - CredentialRequest, - Sender>, -); +#[derive(Debug)] +struct RequestContext { + request: CredentialRequest, + response_channel: Sender>, + request_id: RequestId, +} + +impl RequestContext { + fn send_response(self, response: Result) { + if self.response_channel.send(response).is_err() { + tracing::error!( + "Attempted to send credential response to caller, but channel was closed." + ); + } + } +} #[derive(Debug)] pub struct CredentialService { @@ -91,7 +103,7 @@ impl request: &CredentialRequest, tx: Sender>, ) { - { + let request_id = { let mut cred_request = self.ctx.lock().unwrap(); if cred_request.is_some() { tx.send(Err(CredentialServiceError::Internal( @@ -100,14 +112,24 @@ impl .expect("Send to local receiver to succeed"); return; } else { - _ = cred_request.insert((request.clone(), tx)); + let request_id: RequestId = rand::random(); + let ctx = RequestContext { + request: request.clone(), + response_channel: tx, + request_id, + }; + _ = cred_request.insert(ctx); + request_id } }; let operation = match &request { CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, }; - let view_request = ViewRequest { operation }; + let view_request = ViewRequest { + operation, + id: request_id, + }; let launch_ui_response = self .ui_control_client @@ -118,10 +140,30 @@ impl tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); _ = self.ctx.lock().unwrap().take(); let err = Err(CredentialServiceError::Internal(err)); - let (_, tx) = self.ctx.lock().unwrap().take().unwrap(); - tx.send(err).expect("Request handler to be listening"); + let ctx = self.ctx.lock().unwrap().take().unwrap(); + ctx.response_channel + .send(err) + .expect("Request handler to be listening"); + } + tracing::debug!("Finished setting up request {request_id}"); + } + + pub async fn cancel_request(&self, request_id: RequestId) { + let mut guard = self.ctx.lock().expect("Lock to be taken"); + if let Some(ctx) = guard.take_if(|ctx| ctx.request_id == request_id) { + if request_id == ctx.request_id { + tracing::debug!("Cancelling request {request_id}"); + // TODO: cancel sub-tasks: hybrid and USB streams. + + // It's fine if the requestor is no longer listening for the response. + // TODO: create Cancelled variant + _ = ctx + .response_channel + .send(Err(CredentialServiceError::Internal(format!( + "Cancelled request {request_id}." + )))); + } } - tracing::debug!("Finished setting up request"); } pub async fn get_available_public_key_devices(&self) -> Result, ()> { @@ -132,8 +174,8 @@ impl &self, ) -> Pin + Send + 'static>> { let guard = self.ctx.lock().unwrap(); - if let Some((ref cred_request, _)) = *guard { - let stream = self.hybrid_handler.start(cred_request); + if let Some(RequestContext { ref request, .. }) = *guard { + let stream = self.hybrid_handler.start(request); let ctx = self.ctx.clone(); Box::pin(HybridStateStream { inner: stream, ctx }) } else { @@ -146,8 +188,8 @@ impl pub fn get_usb_credential(&self) -> Pin + Send + 'static>> { let guard = self.ctx.lock().unwrap(); - if let Some((ref cred_request, _)) = *guard { - let stream = self.usb_handler.start(cred_request); + if let Some(RequestContext { ref request, .. }) = *guard { + let stream = self.usb_handler.start(request); let ctx = self.ctx.clone(); Box::pin(UsbStateStream { inner: stream, ctx }) } else { @@ -237,12 +279,8 @@ where } fn complete_request(ctx: &Mutex>, response: CredentialResponse) { - if let Some((_, responder)) = ctx.lock().unwrap().take() { - if responder.send(Ok(response)).is_err() { - tracing::error!( - "Attempted to send credential response to caller, but channel was closed." - ); - } + if let Some(ctx) = ctx.lock().unwrap().take() { + ctx.send_response(Ok(response)); } else { tracing::error!("Tried to consume context to respond to caller, but none was found.") } diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index d547bd3b..fc826eff 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -7,7 +7,7 @@ use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use credentialsd_common::model::{ CredentialRequest, CredentialResponse, Error as CredentialServiceError, WebAuthnError, }; -use credentialsd_common::server::{BackgroundEvent, Device}; +use credentialsd_common::server::{BackgroundEvent, Device, RequestId}; use futures_lite::StreamExt; use tokio::sync::oneshot; use tokio::{ @@ -254,6 +254,11 @@ where Ok(()) } + async fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()> { + self.svc.lock().await.cancel_request(request_id).await; + Ok(()) + } + #[zbus(signal)] async fn state_changed( emitter: &SignalEmitter<'_>, @@ -334,6 +339,7 @@ pub mod test { use credentialsd_common::{ client::FlowController, model::{BackgroundEvent, Device}, + server::RequestId, }; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; @@ -450,9 +456,13 @@ pub mod test { } } - async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + async fn select_credential(&self, _credential_id: String) -> Result<(), ()> { todo!(); } + + async fn cancel_request(&self, _request_id: RequestId) -> Result<(), ()> { + todo!() + } } #[derive(Debug)] @@ -523,12 +533,12 @@ pub mod test { DummyFlowResponse::GetDevices(rsp) } DummyFlowRequest::GetHybridCredential => { - let rsp = self.get_hybrid_credential().await; + self.get_hybrid_credential().await.unwrap(); DummyFlowResponse::GetHybridCredential } DummyFlowRequest::GetUsbCredential => { - let rsp = self.get_usb_credential().await; + self.get_usb_credential().await.unwrap(); DummyFlowResponse::GetUsbCredential } DummyFlowRequest::InitStream => { @@ -656,7 +666,11 @@ pub mod test { Ok(()) } - async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + async fn select_credential(&self, _credential_id: String) -> Result<(), ()> { + todo!(); + } + + async fn cancel_request(&self, _request_id: RequestId) -> Result<(), ()> { todo!(); } } diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index 30b03580..b0670915 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -4,7 +4,7 @@ use std::error::Error; use zbus::{fdo, proxy, Connection}; -use credentialsd_common::server::ViewRequest; +use credentialsd_common::server::{RequestId, ViewRequest}; use crate::credential_service::UiController; @@ -16,6 +16,7 @@ use crate::credential_service::UiController; )] trait UiControlService { fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; + fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()>; } #[derive(Debug)] @@ -181,7 +182,7 @@ pub mod test { .unwrap(); } - pub async fn select_credential(&self, cred_id: String) { + pub async fn select_credential(&self, _cred_id: String) { tracing::debug!( target: "DummyUiServer", "Received select_credential() request" From 6299f844e76e152cdae37740be3f8defa942d876 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 6 Aug 2025 17:12:30 -0500 Subject: [PATCH 3/3] Use infallible conversion to D-Bus BackgroundEvent --- credentialsd-common/src/server.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index d549b6f3..daa87431 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -33,11 +33,9 @@ impl TryFrom for crate::model::BackgroundEvent { } } -// TODO: this can be changed to infallible From -impl TryFrom for BackgroundEvent { - type Error = zvariant::Error; - fn try_from(value: crate::model::BackgroundEvent) -> Result { - let event = match value { +impl From for BackgroundEvent { + fn from(value: crate::model::BackgroundEvent) -> Self { + match value { crate::model::BackgroundEvent::HybridQrStateChanged(state) => { let state: HybridState = state.into(); let value = Value::new(state) @@ -53,8 +51,7 @@ impl TryFrom for BackgroundEvent { BackgroundEvent::UsbStateChanged(value) } - }; - Ok(event) + } } }