diff --git a/credentialsd-common/src/client.rs b/credentialsd-common/src/client.rs index e165c93..6e6573c 100644 --- a/credentialsd-common/src/client.rs +++ b/credentialsd-common/src/client.rs @@ -3,8 +3,8 @@ use std::pin::Pin; use futures_lite::Stream; use crate::{ - model::Device, - server::{BackgroundEvent, RequestId}, + model::{Device, RequestId}, + server::BackgroundEvent, }; /// Used for communication from trusted UI to credential service diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index e32f048..ff7f1c5 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -40,8 +40,17 @@ pub struct Device { #[derive(Clone, Debug, Serialize, Deserialize, Type)] pub enum Operation { - Create, - Get, + PublicKeyCreate, + PublicKeyGet, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +pub struct PortalBackendOptions { + /// Top-level origin of the request if different from the origin. + pub top_origin: Optional, + + /// RP ID of the request. Required for WebAuthn/PublicKey requests. + pub rp_id: Optional, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] @@ -122,6 +131,9 @@ pub struct RequestingParty { pub origin: String, } +/// Identifier for a request to be used for cancellation. +pub type RequestId = u32; + // TODO: Move to credentialsd-ui #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ViewUpdate { @@ -254,6 +266,41 @@ pub enum NfcState { Failed(Error), } +pub enum BackendRequest { + /// Start Hybrid discovery + StartHybridDiscovery, + + /// Start NFC discovery + StartNfcDiscovery, + + /// Start USB discovery + StartUsbDiscovery, + + /// Send client PIN + EnterClientPin(String), + + /// Select a credential by credential ID + SelectCredential(String), + + CancelRequest, +} + +impl std::fmt::Debug for BackendRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::StartHybridDiscovery => write!(f, "StartHybridDiscovery"), + Self::StartNfcDiscovery => write!(f, "StartNfcDiscovery"), + Self::StartUsbDiscovery => write!(f, "StartUsbDiscovery"), + Self::EnterClientPin(_) => f + .debug_tuple("EnterClientPin") + .field(&"******".to_string()) + .finish(), + Self::SelectCredential(arg0) => f.debug_tuple("SelectCredential").field(arg0).finish(), + Self::CancelRequest => write!(f, "CancelRequest"), + } + } +} + #[derive(Debug, Clone)] pub enum Error { /// Some unknown error with the authenticator occurred. diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index dcdd105..f05d999 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -8,10 +8,10 @@ use serde::{ }; use zvariant::{ self, Array, DeserializeDict, DynamicDeserialize, NoneValue, Optional, OwnedValue, - SerializeDict, Signature, Structure, StructureBuilder, Type, Value, signature::Fields, + SerializeDict, Signature, Str, Structure, StructureBuilder, Type, Value, signature::Fields, }; -use crate::model::{Device, Operation, RequestingApplication}; +use crate::model::{BackendRequest, Device, Operation, RequestId, RequestingApplication}; const TAG_VALUE_SIGNATURE: &Signature = &Signature::Structure(Fields::Static { fields: &[&Signature::U32, &Signature::Variant], @@ -49,6 +49,13 @@ const BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED: u32 = 0x80000006; const BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED: u32 = 0x80000007; const BACKGROUND_EVENT_ERROR_PIN_NOT_SET: u32 = 0x80000008; +const BACKEND_REQUEST_START_HYBRID_DISCOVERY: u32 = 0x01; +const BACKEND_REQUEST_START_USB_DISCOVERY: u32 = 0x02; +const BACKEND_REQUEST_START_NFC_DISCOVERY: u32 = 0x03; +const BACKEND_REQUEST_ENTER_CLIENT_PIN: u32 = 0x04; +const BACKEND_REQUEST_SELECT_CREDENTIAL: u32 = 0x05; +const BACKEND_REQUEST_CANCEL_REQUEST: u32 = 0x06; + /// Flattened enum BackgroundEvent for sending across D-Bus. #[derive(Debug, Clone, PartialEq)] pub enum BackgroundEvent { @@ -267,6 +274,94 @@ impl<'de> Deserialize<'de> for BackgroundEvent { } } +impl Type for BackendRequest { + const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE; +} + +impl From<&BackendRequest> for Structure<'_> { + fn from(value: &BackendRequest) -> Self { + match value { + BackendRequest::StartHybridDiscovery => tag_value_to_struct(0x01, None), + BackendRequest::StartNfcDiscovery => tag_value_to_struct(0x02, None), + BackendRequest::StartUsbDiscovery => tag_value_to_struct(0x03, None), + BackendRequest::EnterClientPin(pin) => { + tag_value_to_struct(0x04, Some(Value::Str(pin.into()))) + } + BackendRequest::SelectCredential(credential_id) => { + tag_value_to_struct(0x05, Some(Value::Str(credential_id.into()))) + } + BackendRequest::CancelRequest => tag_value_to_struct(0x06, None), + } + } +} + +impl TryFrom<&Structure<'_>> for BackendRequest { + type Error = zvariant::Error; + + fn try_from(value: &Structure<'_>) -> Result { + let (tag, value) = parse_tag_value_struct(value)?; + + match tag { + 0x01 => Ok(BackendRequest::StartHybridDiscovery), + 0x02 => Ok(BackendRequest::StartNfcDiscovery), + 0x03 => Ok(BackendRequest::StartUsbDiscovery), + 0x04 => { + let s: Str = value.downcast_ref()?; + if s.is_empty() { + return Err(zvariant::Error::invalid_length( + s.len(), + &"a non-empty string", + )); + } + Ok(BackendRequest::EnterClientPin(s.as_str().to_string())) + } + 0x05 => { + let s: Str = value.downcast_ref()?; + if s.is_empty() { + return Err(zvariant::Error::invalid_length( + s.len(), + &"a non-empty string", + )); + } + Ok(BackendRequest::SelectCredential(s.as_str().to_string())) + } + 0x06 => Ok(BackendRequest::CancelRequest), + _ => Err(zvariant::Error::Message(format!( + "Unknown BackendRequest tag : {tag}" + ))), + } + } +} + +impl Serialize for BackendRequest { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let structure: Structure = self.into(); + structure.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for BackendRequest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let d = Structure::deserializer_for_signature(TAG_VALUE_SIGNATURE).map_err(|err| { + D::Error::custom(format!( + "could not create deserializer for tag-value struct: {err}" + )) + })?; + let structure = d.deserialize(deserializer)?; + (&structure).try_into().map_err(|err| { + D::Error::custom(format!( + "could not deserialize structure into BackendRequest: {err}" + )) + }) + } +} + #[derive(Clone, Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] pub struct CreateCredentialRequest { @@ -412,10 +507,7 @@ impl From for GetCredentialResponse { } } -/// Identifier for a request to be used for cancellation. -pub type RequestId = u32; - -#[derive(Serialize, Deserialize, Type)] +#[derive(Clone, Debug, Serialize, Deserialize, Type)] pub struct ViewRequest { pub operation: Operation, @@ -435,7 +527,7 @@ pub struct ViewRequest { pub window_handle: Optional, } -#[derive(Type, PartialEq, Debug)] +#[derive(Clone, Debug, PartialEq, Type)] #[zvariant(signature = "s")] pub enum WindowHandle { Wayland(String), diff --git a/credentialsd-ui/src/client.rs b/credentialsd-ui/src/client.rs index fe8b1cc..ed05c43 100644 --- a/credentialsd-ui/src/client.rs +++ b/credentialsd-ui/src/client.rs @@ -1,5 +1,13 @@ -use async_std::stream::Stream; -use credentialsd_common::{client::FlowController, server::RequestId}; +use async_std::{ + channel::{Receiver, Sender}, + stream::Stream, + sync::Mutex as AsyncMutex, +}; +use credentialsd_common::{ + client::FlowController, + model::{BackendRequest, RequestId}, + server::BackgroundEvent, +}; use futures_lite::StreamExt; use zbus::Connection; @@ -118,3 +126,51 @@ impl FlowController for DbusCredentialClient { Ok(()) } } + +#[derive(Debug)] +pub struct FlowControlClient { + pub tx: Sender, + pub rx: AsyncMutex>>, +} + +impl FlowControlClient { + pub async fn discover_hybrid_authenticators(&self) -> Result<(), ()> { + self.send(BackendRequest::StartHybridDiscovery).await + } + + pub async fn discover_nfc_authenticators(&mut self) -> Result<(), ()> { + self.send(BackendRequest::StartNfcDiscovery).await + } + + pub async fn discover_usb_authenticators(&mut self) -> Result<(), ()> { + self.send(BackendRequest::StartUsbDiscovery).await + } + + pub async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { + self.send(BackendRequest::EnterClientPin(pin)).await + } + + pub async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + self.send(BackendRequest::SelectCredential(credential_id)) + .await + } + + pub async fn cancel_request(&self) -> Result<(), ()> { + self.send(BackendRequest::CancelRequest).await + } + + /// Returns a channel for background events. + /// Can only be called once; returns an error if the subscription has already been taken. + pub async fn subscribe(&mut self) -> Result, ()> { + self.rx.lock().await.take().ok_or_else(|| { + tracing::error!("Subscribe has already been called."); + }) + } + + async fn send(&self, request: BackendRequest) -> Result<(), ()> { + match self.tx.send(request).await { + Ok(_) => Ok(()), + Err(_) => Err(()), + } + } +} diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index b1bdf26..29694a3 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -1,9 +1,29 @@ -use async_std::channel::Sender; +use std::sync::Arc; + +use async_std::{ + channel::{self, Receiver, Sender}, + stream::StreamExt, + sync::Mutex as AsyncMutex, + task::JoinHandle, +}; +use zbus::{ + Connection, ObjectServer, fdo, interface, + message::Header, + names::{BusName, OwnedUniqueName}, + object_server::SignalEmitter, + proxy, + zvariant::{ObjectPath, Optional}, +}; + use credentialsd_common::{ - model::Device, - server::{BackgroundEvent, RequestId, ViewRequest}, + client::FlowController, + model::{ + BackendRequest, Device, Operation, PortalBackendOptions, RequestId, RequestingApplication, + }, + server::{BackgroundEvent, ViewRequest, WindowHandle}, }; -use zbus::{fdo, interface, proxy}; + +use crate::client::{DbusCredentialClient, FlowControlClient}; #[proxy( gen_blocking = false, @@ -31,17 +51,247 @@ pub trait FlowControlService { } pub struct UiControlService { - pub request_tx: Sender, + pub request_tx: Sender<(ViewRequest, Arc>)>, } /// 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: ViewRequest) -> fdo::Result<()> { + async fn launch_ui( + &self, + #[zbus(connection)] conn: &Connection, + request: ViewRequest, + ) -> fdo::Result<()> { tracing::debug!("Received UI launch request"); + let mut client = DbusCredentialClient::new(conn.clone()); + let (fc_tx, fc_rx) = async_std::channel::unbounded(); + let (bg_tx, bg_rx) = async_std::channel::unbounded(); + match client.subscribe().await { + Ok(mut bg_event_stream) => async_std::task::spawn(async move { + while let Some(bg_event) = bg_event_stream.next().await { + if let Err(_) = bg_tx.send(bg_event).await { + tracing::debug!("Background event receiver dropped. Stopping."); + break; + } + } + }), + Err(_) => { + tracing::error!( + ?request, + "Failed to subscribe to background events for request" + ); + return Err(fdo::Error::Failed( + "Failed to subscribe to background events for request".to_string(), + )); + } + }; + async_std::task::spawn(async move { + while let Ok(msg) = fc_rx.recv().await { + // UI doesn't get an error if these fail... + let result = match &msg { + BackendRequest::StartHybridDiscovery => client.get_hybrid_credential().await, + BackendRequest::StartNfcDiscovery => client.get_nfc_credential().await, + BackendRequest::StartUsbDiscovery => client.get_usb_credential().await, + BackendRequest::EnterClientPin(pin) => { + client.enter_client_pin(pin.to_string()).await + } + BackendRequest::SelectCredential(cred_id) => { + client.select_credential(cred_id.to_string()).await + } + BackendRequest::CancelRequest => client.cancel_request(request.id).await, + }; + if let Err(err) = result { + tracing::error!("Failed to send {msg:?} to frontend: {err:?}"); + } + } + client + }); + let flow_control_client = FlowControlClient { + tx: fc_tx, + rx: AsyncMutex::new(Some(bg_rx)), + }; self.request_tx - .send(request) + .send((request, Arc::new(AsyncMutex::new(flow_control_client)))) .await .map_err(|_| fdo::Error::Failed("UI failed to launch".to_string())) } } + +pub struct CredentialPortalBackend { + pub request_tx: Sender<(ViewRequest, Arc>)>, +} + +#[derive(Debug, Clone)] +pub(crate) struct UiContext { + parent_window: Option, + origin: String, + r#type: Operation, + request_id: RequestId, + devices: Vec, + app_id: String, + app_display_name: String, + app_pid: u32, + app_path: String, + options: PortalBackendOptions, +} + +/// These methods are called by the credential service to control the UI. +#[interface(name = "org.freedesktop.impl.portal.experimental.Credential")] +impl CredentialPortalBackend { + async fn initialize( + &self, + #[zbus(header)] header: Header<'_>, + #[zbus(object_server)] object_server: &ObjectServer, + parent_window: Optional, + origin: String, + r#type: Operation, + request_id: RequestId, + devices: Vec, + app_id: String, + app_display_name: String, + app_pid: u32, + app_path: String, + options: PortalBackendOptions, + ) -> fdo::Result> { + let Some(sender) = header.sender() else { + return Err(fdo::Error::BadAddress("Sender not found".to_string())); + }; + let object_path = ObjectPath::from_string_unchecked(format!( + "/org/freedesktop/portal/Credential/{}", + request_id + )); + let ui_context = UiContext { + parent_window: parent_window.into(), + origin, + r#type, + request_id, + devices, + app_id, + app_display_name, + app_pid, + app_path, + options, + }; + let flow_object = FlowObject { + ui_context, + request_tx: self.request_tx.clone(), + return_address: sender.to_owned().into(), + ui_events_forwarder_task: None, + bg_events_tx: None, + }; + object_server.at(object_path.clone(), flow_object).await?; + tracing::debug!("Received UI launch request"); + Ok(object_path) + } +} + +pub struct FlowObject { + ui_context: UiContext, + pub request_tx: Sender<(ViewRequest, Arc>)>, + pub return_address: OwnedUniqueName, + ui_events_forwarder_task: Option>, + bg_events_tx: Option>, +} + +#[interface(name = "org.freedesktop.impl.portal.experimental.Credential.FlowObject")] +impl FlowObject { + /// Start the UI flow with an initial set of available credential interfaces. + /// Call this method after subscribing to the signals. + async fn start( + &mut self, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> fdo::Result<()> { + let (ui_events_tx, ui_events_rx) = channel::bounded(32); + let (bg_events_tx, bg_events_rx) = channel::bounded(32); + let flow_control_client = FlowControlClient { + tx: ui_events_tx, + rx: AsyncMutex::new(Some(bg_events_rx)), + }; + self.bg_events_tx = Some(bg_events_tx); + + let emitter = emitter + .set_destination(BusName::Unique((&self.return_address).into())) + .to_owned(); + let ui_events_task = async_std::task::spawn(async move { + while let Ok(ui_event) = ui_events_rx.recv().await { + tracing::trace!(?ui_event, "Sending UI event signal to portal"); + if emitter.user_interacted(&ui_event).await.is_err() { + tracing::error!("Failed to send UI event signal."); + // TODO: we need to cancel the request here, so we need a + // channel back to the flow object to send the cancellation. + break; + } + } + }); + self.ui_events_forwarder_task = Some(ui_events_task); + + // Assuming this is a PublicKey request, require the rp_id + let rp_id = self + .ui_context + .options + .rp_id + .as_ref() + .ok_or_else(|| { + { + fdo::Error::InvalidArgs( + "rp_id is required for public key credential requests".to_string(), + ) + } + })? + .to_string(); + let req = ( + ViewRequest { + operation: self.ui_context.r#type.clone(), + id: self.ui_context.request_id, + rp_id, + requesting_app: RequestingApplication { + path_or_app_id: self.ui_context.app_id.clone(), + name: Some(self.ui_context.app_display_name.clone()).into(), + pid: self.ui_context.app_pid, + }, + initial_devices: self.ui_context.devices.clone(), + window_handle: self.ui_context.parent_window.clone().into(), + }, + Arc::new(AsyncMutex::new(flow_control_client)), + ); + if self.request_tx.send(req).await.is_err() { + tracing::error!("Received message to start flow, but GUI thread is not listening."); + return Err(fdo::Error::Failed("Failed to start GUI".to_string())); + } + Ok(()) + } + + async fn notify_state_changed(&self, event: BackgroundEvent) -> fdo::Result<()> { + tracing::trace!(?event, "Received background event"); + if let Some(tx) = &self.bg_events_tx { + if tx.send(event).await.is_ok() { + return Ok(()); + } + tracing::error!("Failed to send event to GUI thread"); + } else { + tracing::error!("Flow was not properly initialized before receiving events."); + } + return Err(fdo::Error::Failed("Failed to handle event".to_string())); + } + + async fn cancel( + &mut self, + #[zbus(header)] header: Header<'_>, + #[zbus(object_server)] object_server: &ObjectServer, + ) -> fdo::Result<()> { + if let Some(task) = self.ui_events_forwarder_task.take() { + task.cancel().await; + } + if let Some(path) = header.path() { + // TODO: Send clean up task to GUI thread. + object_server.remove::(path).await?; + } + Ok(()) + } + + #[zbus(signal)] + async fn user_interacted( + emitter: SignalEmitter<'_>, + event: &BackendRequest, + ) -> zbus::Result<()>; +} diff --git a/credentialsd-ui/src/gui/mod.rs b/credentialsd-ui/src/gui/mod.rs index bd65aed..e7e3e10 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -5,28 +5,27 @@ use std::{sync::Arc, thread::JoinHandle}; use async_std::{channel::Receiver, sync::Mutex as AsyncMutex}; -use credentialsd_common::server::{ViewRequest, WindowHandle}; -use credentialsd_common::{client::FlowController, model::ViewUpdate}; +use credentialsd_common::{ + model::ViewUpdate, + server::{ViewRequest, WindowHandle}, +}; + +use crate::client::FlowControlClient; use view_model::ViewEvent; -pub(super) fn start_gui_thread( - rx: Receiver, - flow_controller: F, +pub(super) fn start_gui_thread( + rx: Receiver<(ViewRequest, Arc>)>, ) -> Result, std::io::Error> { thread::Builder::new().name("gui".into()).spawn(move || { - let flow_controller = Arc::new(AsyncMutex::new(flow_controller)); // D-Bus received a request and needs a window open - while let Ok(view_request) = rx.recv_blocking() { - run_gui(flow_controller.clone(), view_request); + while let Ok((view_request, flow_controller)) = rx.recv_blocking() { + run_gui(flow_controller, view_request); } }) } -fn run_gui( - flow_controller: Arc>, - request: ViewRequest, -) { +fn run_gui(flow_controller: Arc>, request: ViewRequest) { let parent_window: Option = request.window_handle.as_ref().and_then(|h| { h.to_string() .try_into() @@ -43,11 +42,8 @@ fn run_gui( vm.start_event_loop().await; tracing::debug!("Finishing user request."); // If cancellation fails, that's fine. - let _ = flow_controller - .lock() - .await - .cancel_request(request_id) - .await; + let _ = flow_controller.lock().await.cancel_request().await; + // TODO: Clean up flow_object when request completes }); view_model::gtk::start_gtk_app(parent_window, tx_event, rx_update); diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 6ca166f..49a213e 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -13,17 +13,15 @@ use gettextrs::gettext; use serde::{Deserialize, Serialize}; use tracing::{error, info}; -use credentialsd_common::{ - client::FlowController, - model::{Device, Error, HybridState, NfcState, Operation, Transport, UsbState, ViewUpdate}, +use credentialsd_common::model::{ + Device, Error, HybridState, NfcState, Operation, Transport, UsbState, ViewUpdate, }; +use crate::client::FlowControlClient; + #[derive(Debug)] -pub(crate) struct ViewModel -where - F: FlowController + Send, -{ - flow_controller: Arc>, +pub(crate) struct ViewModel { + flow_controller: Arc>, tx_update: Sender, rx_event: Receiver, title: String, @@ -44,10 +42,10 @@ where // hybrid_linked_state: HybridState, } -impl ViewModel { +impl ViewModel { pub(crate) fn new( request: ViewRequest, - flow_controller: Arc>, + flow_controller: Arc>, rx_event: Receiver, tx_update: Sender, ) -> Self { @@ -79,11 +77,11 @@ impl ViewModel { async fn update_title(&mut self) { let mut title = match self.operation { - Operation::Create => { + Operation::PublicKeyCreate => { // TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from gettext("Create a passkey for %s1") } - Operation::Get => { + Operation::PublicKeyGet => { // TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from gettext("Use a passkey for %s1") } @@ -92,14 +90,14 @@ impl ViewModel { title = title.replace("%s1", &self.rp_id); let mut subtitle = match self.operation { - Operation::Create => { + Operation::PublicKeyCreate => { // TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from // TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold // TRANSLATORS: %i1 is the process ID of the requesting application // TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application gettext("\"%s2\" (process ID: %i1, binary: %s3) is asking to create a credential to register at \"%s1\". Only proceed if you trust this process.") } - Operation::Get => { + Operation::PublicKeyGet => { // TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from // TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold // TRANSLATORS: %i1 is the process ID of the requesting application @@ -160,15 +158,15 @@ impl ViewModel { match device.transport { Transport::Usb => { let mut cred_service = self.flow_controller.lock().await; - (*cred_service).get_usb_credential().await.unwrap(); + (*cred_service).discover_usb_authenticators().await.unwrap(); } Transport::Nfc => { let mut cred_service = self.flow_controller.lock().await; - (*cred_service).get_nfc_credential().await.unwrap(); + (*cred_service).discover_nfc_authenticators().await.unwrap(); } Transport::HybridQr => { - let mut cred_service = self.flow_controller.lock().await; - cred_service.get_hybrid_credential().await.unwrap(); + let cred_service = self.flow_controller.lock().await; + cred_service.discover_hybrid_authenticators().await.unwrap(); } _ => { todo!() @@ -234,9 +232,12 @@ impl ViewModel { Event::Background(BackgroundEvent::UsbConnected) => { info!("Found USB device") } - + // TODO: Add this event + // Event::Background(BackgroundEvent::DevicesUpdated(devices)) => { + // self.update_devices(devices).await + // } Event::Background(BackgroundEvent::NeedsPin { attempts_left }) => { - // TODO: UsbNeedsPin just needs to be NeedsPing + // TODO: UsbNeedsPin just needs to be NeedsPin self.tx_update .send(ViewUpdate::UsbNeedsPin { attempts_left }) .await diff --git a/credentialsd-ui/src/main.rs b/credentialsd-ui/src/main.rs index 701e0ba..6aa2aa6 100644 --- a/credentialsd-ui/src/main.rs +++ b/credentialsd-ui/src/main.rs @@ -6,7 +6,7 @@ mod gui; use std::error::Error; -use crate::{client::DbusCredentialClient, dbus::UiControlService}; +use crate::dbus::{CredentialPortalBackend, UiControlService}; fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); @@ -19,18 +19,20 @@ async fn run() -> Result<(), Box> { let (request_tx, request_rx) = async_std::channel::bounded(2); // this allows the D-Bus service to signal to the GUI to draw a window for // executing the credential flow. - let client_conn = zbus::connection::Builder::session()?.build().await?; - let cred_client = DbusCredentialClient::new(client_conn); - let _handle = gui::start_gui_thread(request_rx, cred_client)?; + let _handle = gui::start_gui_thread(request_rx)?; println!(" ✅"); print!("Starting UI Control listener...\t"); - let interface = UiControlService { request_tx }; + let interface = UiControlService { + request_tx: request_tx.clone(), + }; + let portal_backend_interface = CredentialPortalBackend { request_tx }; let path = "/xyz/iinuwa/credentialsd/UiControl"; let service = "xyz.iinuwa.credentialsd.UiControl"; let _server_conn = zbus::connection::Builder::session()? .name(service)? .serve_at(path, interface)? + .serve_at("/org/freedesktop/portal/desktop", portal_backend_interface)? .build() .await?; println!(" ✅"); diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index 1ab7979..bdd4416 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -3,26 +3,22 @@ pub mod nfc; pub mod usb; use std::{ - error::Error, fmt::Debug, - future::Future, pin::Pin, sync::{Arc, Mutex}, task::Poll, }; +use async_trait::async_trait; use futures_lite::{FutureExt, Stream, StreamExt}; use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, }; use nfc::{NfcEvent, NfcHandler, NfcState, NfcStateInternal}; -use tokio::sync::oneshot::Sender; +use tokio::sync::oneshot; -use credentialsd_common::{ - model::{Device, Error as CredentialServiceError, Operation, RequestingApplication, Transport}, - server::{RequestId, ViewRequest, WindowHandle}, -}; +use credentialsd_common::model::{Device, Error as CredentialServiceError, RequestId, Transport}; use crate::{ credential_service::{hybrid::HybridEvent, usb::UsbEvent}, @@ -36,18 +32,10 @@ use self::{ pub use usb::UsbState; -/// Used by the credential service to control the UI. -pub trait UiController { - fn launch_ui( - &self, - request: ViewRequest, - ) -> impl Future>> + Send; -} - #[derive(Debug)] struct RequestContext { request: CredentialRequest, - response_channel: Sender>, + response_channel: oneshot::Sender>, request_id: RequestId, } @@ -61,106 +49,76 @@ impl RequestContext { } } +/// Manages request to authenticator devices. +#[async_trait] +pub trait ManageDevice { + async fn init_request( + &self, + request: &CredentialRequest, + tx: oneshot::Sender>, + ) -> Result; + async fn cancel_request(&self, request_id: RequestId); + async fn get_available_public_key_devices(&self) -> Result, ()>; + async fn get_hybrid_credential( + &self, + ) -> Pin + Send + 'static>>; + async fn get_nfc_credential(&self) -> Pin + Send + 'static>>; + async fn get_usb_credential(&self) -> Pin + Send + 'static>>; +} + #[derive(Debug)] -pub struct CredentialService { +pub struct CredentialService { /// Current request and channel to respond to caller. ctx: Arc>>, - hybrid_handler: H, - usb_handler: U, - nfc_handler: N, - - ui_control_client: Arc, + hybrid_handler: Mutex, + nfc_handler: Mutex, + usb_handler: Mutex, } -impl< - H: HybridHandler + Debug, - U: UsbHandler + Debug, - N: NfcHandler + Debug, - UC: UiController + Debug, - > CredentialService +impl + CredentialService { - pub fn new( - hybrid_handler: H, - usb_handler: U, - nfc_handler: N, - ui_control_client: Arc, - ) -> Self { + pub fn new(hybrid_handler: H, nfc_handler: N, usb_handler: U) -> Self { Self { ctx: Arc::new(Mutex::new(None)), - hybrid_handler, - usb_handler, - nfc_handler, - - ui_control_client, + hybrid_handler: Mutex::new(hybrid_handler), + nfc_handler: Mutex::new(nfc_handler), + usb_handler: Mutex::new(usb_handler), } } +} - pub async fn init_request( +#[async_trait] +impl ManageDevice + for CredentialService +{ + async fn init_request( &self, request: &CredentialRequest, - requesting_app: Option, - window_handle: Option, - tx: Sender>, - ) { - let request_id = { - let mut cred_request = self.ctx.lock().unwrap(); - if cred_request.is_some() { - tx.send(Err(CredentialServiceError::Internal( - "Already a request in progress.".to_string(), - ))) - .expect("Send to local receiver to succeed"); - return; - } else { - 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 rp_id = match &request { - CredentialRequest::CreatePublicKeyCredentialRequest(r) => r.relying_party.id.clone(), - CredentialRequest::GetPublicKeyCredentialRequest(r) => r.relying_party_id.clone(), - }; - let initial_devices = self - .get_available_public_key_devices() - .await - .unwrap_or_default(); - let view_request = ViewRequest { - operation, - id: request_id, - rp_id, - initial_devices, - requesting_app: requesting_app.unwrap_or_default(), // We can't send Options, so we send an empty string instead, if we don't know the peer - window_handle: window_handle.into(), - }; - - let launch_ui_response = self - .ui_control_client - .launch_ui(view_request) - .await - .map_err(|err| err.to_string()); - if let Err(err) = launch_ui_response { - tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); - let err = Err(CredentialServiceError::Internal(err)); - let ctx = self.ctx.lock().unwrap().take().unwrap(); - ctx.response_channel - .send(err) - .expect("Request handler to be listening"); + tx: oneshot::Sender>, + ) -> Result { + let mut cred_request = self.ctx.lock().unwrap(); + if cred_request.is_some() { + Err(CredentialServiceError::Internal( + "Already a request in progress.".to_string(), + )) + } else { + let request_id: RequestId = rand::random(); + // TODO: Spawn a task here that will listen to the signals from ui_control_client. + // Move the get_*_credential(), etc. from gateway to here. + let ctx = RequestContext { + request: request.clone(), + response_channel: tx, + request_id, + }; + _ = cred_request.insert(ctx); + Ok(request_id) } - tracing::debug!("Finished setting up request {request_id}"); } - pub async fn cancel_request(&self, request_id: RequestId) { + 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 { @@ -178,7 +136,7 @@ impl< } } - pub async fn get_available_public_key_devices(&self) -> Result, ()> { + async fn get_available_public_key_devices(&self) -> Result, ()> { // We create the list new for each call, in case someone plugs in // an NFC-reader in the middle of an auth-flow let mut devices = vec![ @@ -200,12 +158,12 @@ impl< Ok(devices) } - pub fn get_hybrid_credential( + async fn get_hybrid_credential( &self, ) -> Pin + Send + 'static>> { let guard = self.ctx.lock().unwrap(); if let Some(RequestContext { ref request, .. }) = *guard { - let stream = self.hybrid_handler.start(request); + let stream = self.hybrid_handler.lock().unwrap().start(request); let ctx = self.ctx.clone(); Box::pin(HybridStateStream { inner: stream, ctx }) } else { @@ -216,10 +174,10 @@ impl< } } - pub fn get_usb_credential(&self) -> Pin + Send + 'static>> { + async fn get_usb_credential(&self) -> Pin + Send + 'static>> { let guard = self.ctx.lock().unwrap(); if let Some(RequestContext { ref request, .. }) = *guard { - let stream = self.usb_handler.start(request); + let stream = self.usb_handler.lock().unwrap().start(request); let ctx = self.ctx.clone(); Box::pin(UsbStateStream { inner: stream, ctx }) } else { @@ -230,10 +188,10 @@ impl< } } - pub fn get_nfc_credential(&self) -> Pin + Send + 'static>> { + async fn get_nfc_credential(&self) -> Pin + Send + 'static>> { let guard = self.ctx.lock().unwrap(); if let Some(RequestContext { ref request, .. }) = *guard { - let stream = self.nfc_handler.start(request); + let stream = self.nfc_handler.lock().unwrap().start(request); let ctx = self.ctx.clone(); Box::pin(NfcStateStream { inner: stream, ctx }) } else { @@ -403,62 +361,14 @@ mod test { use super::{ hybrid::{test::DummyHybridHandler, HybridStateInternal}, nfc::InProcessNfcHandler, - AuthenticatorResponse, CredentialService, + AuthenticatorResponse, CredentialService, ManageDevice, }; - #[test] - fn test_hybrid_sets_credential() { - tracing_subscriber::fmt::init(); - let request = create_credential_request(); - let qr_code = String::from("FIDO:/078241338926040702789239694720083010994762289662861130514766991835876383562063181103169246410435938367110394959927031730060360967994421343201235185697538107096654083332"); - let authenticator_response = create_authenticator_response(); - - let (request_tx, request_rx) = oneshot::channel(); - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async move { - let hybrid_handler = DummyHybridHandler::new(vec![ - HybridStateInternal::Init(qr_code), - HybridStateInternal::Connecting, - HybridStateInternal::Completed(Box::new(authenticator_response)), - ]); - let usb_handler = InProcessUsbHandler {}; - let nfc_handler = InProcessNfcHandler {}; - let (ui_server, ui_client) = DummyUiServer::new(Vec::new()); - let ui_server = Arc::new(ui_server); - let user = ui_server.clone(); - let cred_service = Arc::new(AsyncMutex::new(CredentialService::new( - hybrid_handler, - usb_handler, - nfc_handler, - Arc::new(ui_client), - ))); - let (mut flow_server, flow_client) = DummyFlowServer::new(cred_service.clone()); - ui_server.init(flow_client).await; - - tokio::spawn(async move { ui_server.run().await }); - tokio::spawn(async move { flow_server.run().await }); - cred_service - .lock() - .await - .init_request(&request, None, None, request_tx) - .await; - user.request_hybrid_credential().await; - tokio::time::timeout(Duration::from_secs(5), request_rx) - .await - .expect("request to complete") - .expect("response to be sent") - .expect("a credential to be returned"); - }); - } - fn create_credential_request() -> CredentialRequest { let challenge = "Ox0AXQz7WUER7BGQFzvVrQbReTkS3sepVGj26qfUhhrWSarkDbGF4T4NuCY1aAwHYzOzKMJJ2YRSatetl0D9bQ"; let origin = NavigationContext::SameOrigin("https://webauthn.io".parse().unwrap()); let client_data_json = - webauthn::format_client_data_json(Operation::Create, challenge, &origin); + webauthn::format_client_data_json(Operation::PublicKeyCreate, challenge, &origin); let client_data_hash = webauthn::create_client_data_hash(&client_data_json); let make_request = MakeCredentialRequest { hash: client_data_hash, diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index 11fa85b..079809b 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -1,14 +1,16 @@ //! This module implements the service to allow the user to control the flow of //! the credential request through the trusted UI. +use std::sync::Mutex; use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use async_trait::async_trait; use credentialsd_common::model::{ - Device, Error as CredentialServiceError, RequestingApplication, WebAuthnError, + BackendRequest, Device, Error as CredentialServiceError, Operation, PortalBackendOptions, + RequestId, RequestingApplication, WebAuthnError, }; -use credentialsd_common::server::{BackgroundEvent, RequestId, WindowHandle}; -use futures_lite::StreamExt; +use credentialsd_common::server::{BackgroundEvent, ViewRequest, WindowHandle}; +use futures_lite::{Stream, StreamExt}; use tokio::sync::oneshot; use tokio::{ sync::{ @@ -24,42 +26,36 @@ use zbus::{ ObjectServer, }; +use crate::credential_service::ManageDevice; +use crate::dbus::ui_control::Flow; +use crate::dbus::UiControlServiceClient; use crate::{ - credential_service::{ - hybrid::{HybridHandler, HybridState}, - nfc::{NfcHandler, NfcState}, - usb::UsbHandler, - CredentialService, UiController, UsbState, - }, + credential_service::{hybrid::HybridState, nfc::NfcState, UsbState}, + dbus::ui_control::UiController, model::{CredentialRequest, CredentialResponse}, }; pub const SERVICE_PATH: &str = "/xyz/iinuwa/credentialsd/FlowControl"; pub const SERVICE_NAME: &str = "xyz.iinuwa.credentialsd.FlowControl"; -pub async fn start_flow_control_service< - H: HybridHandler + Debug + Send + Sync + 'static, - U: UsbHandler + Debug + Send + Sync + 'static, - N: NfcHandler + Debug + Send + Sync + 'static, - UC: UiController + Debug + Send + Sync + 'static, ->( - credential_service: CredentialService, +pub async fn start_flow_control_service( + device_manager: M, ) -> zbus::Result<( Connection, Sender<( CredentialRequest, - Option, // Application name sending the request - Option, // Client window handle + RequestingApplication, + Option, // Client window handle oneshot::Sender>, )>, )> { - let svc = Arc::new(AsyncMutex::new(credential_service)); + let svc = Arc::new(AsyncMutex::new(device_manager)); let svc2 = svc.clone(); let conn = Builder::session()? .name(SERVICE_NAME)? .serve_at( SERVICE_PATH, - FlowControlService { + FlowControlDbusService { signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), svc, pin_tx: Arc::new(AsyncMutex::new(None)), @@ -71,22 +67,203 @@ pub async fn start_flow_control_service< )? .build() .await?; - let (initiator_tx, mut initiator_rx) = mpsc::channel(2); + let (initiator_tx, mut initiator_rx) = mpsc::channel::<( + CredentialRequest, + RequestingApplication, + Option, + oneshot::Sender>, + )>(2); + let conn2 = conn.clone(); tokio::spawn(async move { - let svc = svc2; while let Some((msg, requesting_app, window_handle, tx)) = initiator_rx.recv().await { - svc.lock() - .await - .init_request(&msg, requesting_app, window_handle, tx) - .await; + let svc = svc2.clone(); + let ui_control_client = UiControlServiceClient::new(conn2.clone()); + if let Err(_) = + tx.send(handle(svc, ui_control_client, msg, requesting_app, window_handle).await) + { + tracing::error!( + "Received response to credential request, but failed to forward it to gateway" + ); + } } }); Ok((conn, initiator_tx)) } -struct FlowControlService { +async fn handle( + svc: Arc>, + ui_control_client: UC, + msg: CredentialRequest, + requesting_app: RequestingApplication, + window_handle: Option, +) -> Result { + let (request_tx, request_rx) = oneshot::channel(); + let request_id = svc.lock().await.init_request(&msg, request_tx).await?; + let operation = match &msg { + CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::PublicKeyCreate, + CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::PublicKeyGet, + }; + let rp_id = match &msg { + CredentialRequest::CreatePublicKeyCredentialRequest(r) => r.relying_party.id.clone(), + CredentialRequest::GetPublicKeyCredentialRequest(r) => r.relying_party_id.clone(), + }; + + // TODO: pass origin to this method so we can do this correctly. + let origin = match &msg { + CredentialRequest::CreatePublicKeyCredentialRequest(r) => r.origin.clone(), + CredentialRequest::GetPublicKeyCredentialRequest(r) => { + format!("https://{}", r.relying_party_id.clone()) + } + }; + + // TODO: pass top_origin to this method so we can do this correctly. + let top_origin = match &msg { + CredentialRequest::CreatePublicKeyCredentialRequest(r) => None, + CredentialRequest::GetPublicKeyCredentialRequest(r) => None, + }; + let initial_devices = svc + .lock() + .await + .get_available_public_key_devices() + .await + .unwrap_or_default(); + + let RequestingApplication { + path_or_app_id, + name: app_name, + pid: app_pid, + } = requesting_app; + let app_name = Option::from(app_name).unwrap_or_else(|| "TODO: Require app name".to_string()); + let flow = match ui_control_client + .initialize( + window_handle, + origin, + operation, + request_id, + initial_devices, + path_or_app_id.clone(), + app_name, + app_pid, + // TODO: Make path and app ID separate. + path_or_app_id, + PortalBackendOptions { + top_origin: top_origin.into(), + rp_id: Some(rp_id).into(), + }, + ) + .await + { + Ok(rx) => rx, + Err(err) => { + tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); + return Err(CredentialServiceError::Internal(err.to_string())); + } + }; + tokio::spawn(async move { + let client_pin_tx: Arc>>> = Arc::new(Mutex::new(None)); + let cred_selector_tx = Arc::new(Mutex::new(None)); + while let Some(ui_request) = flow.receive_ui_event().await { + match ui_request { + BackendRequest::StartHybridDiscovery => { + let stream = svc + .lock() + .await + .get_hybrid_credential() + .await + .map(|state| (&state).into()); + let flow = flow.clone(); + forward_background_event_stream(flow, stream); + } + BackendRequest::StartNfcDiscovery => { + let stream = svc + .lock() + .await + .get_nfc_credential() + .await + .map(|state| (&state).into()); + let flow = flow.clone(); + forward_background_event_stream(flow, stream); + } + BackendRequest::StartUsbDiscovery => { + let client_pin_tx = client_pin_tx.clone(); + let cred_selector_tx = cred_selector_tx.clone(); + let stream = + svc.lock() + .await + .get_usb_credential() + .await + .map(move |usb_state| { + match &usb_state { + UsbState::NeedsPin { pin_tx, .. } => { + *client_pin_tx.lock().unwrap() = Some(pin_tx.clone()); + } + UsbState::SelectingCredential { cred_tx, .. } => { + *cred_selector_tx.lock().unwrap() = Some(cred_tx.clone()); + } + _ => {} + } + (&usb_state).into() + }); + let flow = flow.clone(); + forward_background_event_stream(flow, stream); + } + BackendRequest::EnterClientPin(pin) => { + let tx = { client_pin_tx.lock().unwrap().take() }; + if let Some(tx) = tx { + if tx.send(pin).await.is_err() { + tracing::error!("Failed to send client PIN to device"); + } + } else { + tracing::error!( + "Invalid state: received a client PIN with no pending request." + ); + } + } + BackendRequest::SelectCredential(id) => { + let tx = { cred_selector_tx.lock().unwrap().take() }; + if let Some(tx) = tx { + if tx.send(id).await.is_err() { + tracing::error!("Failed to send credential selection to device"); + } + } else { + tracing::error!( + "Invalid state: received a credential selection ID with no pending request." + ); + } + } + BackendRequest::CancelRequest => { + tracing::debug!(%request_id, "Cancelling request"); + svc.lock().await.cancel_request(request_id).await; + } + } + } + }); + tracing::debug!("Finished setting up request {request_id}"); + let cred_response = request_rx + .await + .expect("Credential service not to drop request channel before responding."); + let f = cred_response.map_err(|err| err.into()); + f +} + +fn forward_background_event_stream( + flow: Flow, + mut stream: impl Stream + Send + Unpin + 'static, +) { + tokio::spawn(async move { + while let Some(event) = stream.next().await { + let send_result = flow.send_state_update(event).await; + if send_result.is_err() { + tracing::error!("Failed to send state update event to backend. Stopping flow"); + break; + } + } + }); +} + +struct FlowControlService { + svc: Arc>, signal_state: Arc>, - svc: Arc>>, pin_tx: Arc>>>, cred_tx: Arc>>>, usb_event_forwarder_task: Arc>>, @@ -94,6 +271,23 @@ struct FlowControlService>>, } +impl FlowControlService { + fn send_update(&self) {} +} + +struct FlowControlDbusService { + svc: Arc>, + + signal_state: Arc>, + + cred_tx: Arc>>>, + pin_tx: Arc>>>, + + hybrid_event_forwarder_task: Arc>>, + nfc_event_forwarder_task: Arc>>, + usb_event_forwarder_task: Arc>>, +} + /// The following methods are for communication between the [trusted] /// UI and the credential service, and should not be called by arbitrary /// clients. @@ -105,12 +299,9 @@ struct FlowControlService FlowControlService +impl FlowControlDbusService where - H: HybridHandler + Debug + Send + Sync + 'static, - U: UsbHandler + Debug + Send + Sync + 'static, - N: NfcHandler + Debug + Send + Sync + 'static, - UC: UiController + Debug + Send + Sync + 'static, + M: ManageDevice + Debug + Send + Sync + 'static, { async fn subscribe( &self, @@ -148,11 +339,11 @@ where #[zbus(object_server)] object_server: &ObjectServer, ) -> fdo::Result<()> { let svc = self.svc.lock().await; - let mut stream = svc.get_hybrid_credential(); + let mut stream = svc.get_hybrid_credential().await; let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: zbus::Result>> = + let interface: zbus::Result>> = object_server.interface(SERVICE_PATH).await; let emitter = match interface { @@ -187,13 +378,13 @@ where &self, #[zbus(object_server)] object_server: &ObjectServer, ) -> fdo::Result<()> { - let mut stream = self.svc.lock().await.get_usb_credential(); + let mut stream = self.svc.lock().await.get_usb_credential().await; let usb_pin_tx = self.pin_tx.clone(); let usb_cred_tx = self.cred_tx.clone(); let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: zbus::Result>> = + let interface: zbus::Result>> = object_server.interface(SERVICE_PATH).await; let emitter = match interface { @@ -236,13 +427,13 @@ where &self, #[zbus(object_server)] object_server: &ObjectServer, ) -> fdo::Result<()> { - let mut stream = self.svc.lock().await.get_nfc_credential(); + let mut stream = self.svc.lock().await.get_nfc_credential().await; let nfc_pin_tx = self.pin_tx.clone(); let nfc_cred_tx = self.cred_tx.clone(); let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: zbus::Result>> = + let interface: zbus::Result>> = object_server.interface(SERVICE_PATH).await; let emitter = match interface { @@ -343,7 +534,7 @@ enum SignalState { pub trait CredentialRequestController { async fn request_credential( &self, - requesting_app: Option, + requesting_app: RequestingApplication, request: CredentialRequest, window_handle: Option, ) -> Result; @@ -352,8 +543,8 @@ pub trait CredentialRequestController { pub struct CredentialRequestControllerClient { pub initiator: Sender<( CredentialRequest, - Option, // Application name sending the request - Option, // Client window handle, + RequestingApplication, // Application name sending the request + Option, // Client window handle, oneshot::Sender>, )>, } @@ -362,7 +553,7 @@ pub struct CredentialRequestControllerClient { impl CredentialRequestController for CredentialRequestControllerClient { async fn request_credential( &self, - requesting_app: Option, + requesting_app: RequestingApplication, request: CredentialRequest, window_handle: Option, ) -> Result { @@ -399,18 +590,13 @@ pub mod test { use credentialsd_common::{ client::FlowController, - model::Device, - server::{BackgroundEvent, RequestId}, + model::{Device, RequestId}, + server::BackgroundEvent, }; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; - use crate::credential_service::{ - hybrid::{HybridHandler, HybridState}, - nfc::{NfcHandler, NfcState}, - usb::UsbHandler, - CredentialService, UiController, UsbState, - }; + use crate::credential_service::{hybrid::HybridState, nfc::NfcState, ManageDevice, UsbState}; #[allow(clippy::enum_variant_names)] #[derive(Debug)] @@ -540,15 +726,12 @@ pub mod test { } #[derive(Debug)] - pub struct DummyFlowServer + pub struct DummyFlowServer where - H: HybridHandler + Debug + Send + Sync, - U: UsbHandler + Debug + Send + Sync, - N: NfcHandler + Debug + Send + Sync, - UC: UiController + Debug + Send + Sync, + M: ManageDevice, { rx: mpsc::Receiver<(DummyFlowRequest, oneshot::Sender)>, - svc: Arc>>, + svc: Arc>, bg_event_tx: Option>, pin_tx: Arc>>>, usb_event_forwarder_task: Arc>>, @@ -556,13 +739,7 @@ pub mod test { hybrid_event_forwarder_task: Arc>>, } - impl< - H: HybridHandler + Debug + Send + Sync, - U: UsbHandler + Debug + Send + Sync, - N: NfcHandler + Debug + Send + Sync, - UC: UiController + Debug + Send + Sync, - > DummyFlowServer - { + impl DummyFlowServer { /* async fn send(&self, request: ManagementRequest) -> Result { let (response_tx, response_rx) = oneshot::channel(); @@ -583,9 +760,7 @@ pub mod test { } } */ - pub fn new( - svc: Arc>>, - ) -> (Self, DummyFlowClient) { + pub fn new(svc: Arc>) -> (Self, DummyFlowClient) { let (request_tx, request_rx) = mpsc::channel(32); let server = Self { rx: request_rx, @@ -648,7 +823,7 @@ pub mod test { async fn get_hybrid_credential(&mut self) -> Result<(), ()> { let svc = self.svc.lock().await; - let mut stream = svc.get_hybrid_credential(); + let mut stream = svc.get_hybrid_credential().await; tracing::debug!(target: "DummyFlowServer", "Subscribing to hybrid credential state changes"); if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { let task = tokio::spawn(async move { @@ -683,7 +858,7 @@ pub mod test { } async fn get_usb_credential(&mut self) -> Result<(), ()> { - let mut stream = self.svc.lock().await.get_usb_credential(); + let mut stream = self.svc.lock().await.get_usb_credential().await; if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { let usb_pin_tx = self.pin_tx.clone(); let task = tokio::spawn(async move { @@ -718,7 +893,7 @@ pub mod test { } async fn get_nfc_credential(&mut self) -> Result<(), ()> { - let mut stream = self.svc.lock().await.get_nfc_credential(); + let mut stream = self.svc.lock().await.get_nfc_credential().await; if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { let nfc_pin_tx = self.pin_tx.clone(); let task = tokio::spawn(async move { @@ -783,13 +958,7 @@ pub mod test { } } - impl< - H: HybridHandler + Debug + Send + Sync, - U: UsbHandler + Debug + Send + Sync, - N: NfcHandler + Debug + Send + Sync, - UC: UiController + Debug + Send + Sync, - > Drop for DummyFlowServer - { + impl Drop for DummyFlowServer { fn drop(&mut self) { if let Some(task) = self.usb_event_forwarder_task.lock().unwrap().take() { task.abort(); diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index b94ff04..8383d2c 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -1,12 +1,44 @@ //! These methods are called by the flow controller to launch the trusted UI. -use std::error::Error; +use std::{error::Error, future::Future, sync::Arc}; -use zbus::{fdo, proxy, Connection}; +use futures_lite::StreamExt; +use tokio::sync::{ + mpsc::{self, Receiver}, + Mutex as AsyncMutex, +}; +use zbus::{ + fdo, proxy, + zvariant::{ObjectPath, Optional, OwnedObjectPath}, + Connection, +}; -use credentialsd_common::server::{RequestId, ViewRequest}; +use credentialsd_common::{ + model::{BackendRequest, Device, Operation, PortalBackendOptions, RequestId}, + server::{BackgroundEvent, ViewRequest, WindowHandle}, +}; -use crate::credential_service::UiController; +/// Used by the credential service to control the UI. +pub trait UiController { + fn launch_ui( + &self, + request: ViewRequest, + ) -> impl Future>> + Send; + + fn initialize( + &self, + parent_window: Option, + origin: String, + r#type: Operation, + request_id: RequestId, + devices: Vec, + app_id: String, + app_display_name: String, + app_pid: u32, + app_path: String, + options: PortalBackendOptions, + ) -> impl Future>> + Send; +} #[proxy( gen_blocking = false, @@ -19,6 +51,67 @@ trait UiControlService { fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()>; } +#[proxy( + gen_blocking = false, + interface = "org.freedesktop.impl.portal.experimental.Credential", + default_service = "xyz.iinuwa.credentialsd.UiControl", + default_path = "/org/freedesktop/portal/desktop" +)] +trait UiControlService2 { + fn initialize( + &self, + parent_window: Optional, + origin: String, + r#type: Operation, + request_id: RequestId, + devices: Vec, + app_id: String, + app_display_name: String, + app_pid: u32, + app_path: String, + options: PortalBackendOptions, + ) -> fdo::Result; +} + +#[derive(Clone, Debug)] +pub struct Flow { + proxy: Arc>, + ui_events_rx: Arc>>, +} + +impl Flow { + pub async fn receive_ui_event(&self) -> Option { + self.ui_events_rx.lock().await.recv().await + } + + pub async fn send_state_update(&self, event: BackgroundEvent) -> Result<(), ()> { + if let Err(err) = self.proxy.notify_state_changed(event).await { + match err { + fdo::Error::UnknownObject(description) => { + tracing::error!(%description, "Flow D-Bus object no longer available at path"); + } + _ => tracing::error!(%err, "Failed to send update to backend"), + } + return Err(()); + } + Ok(()) + } +} +#[proxy( + gen_blocking = false, + interface = "org.freedesktop.impl.portal.experimental.Credential.FlowObject", + default_service = "xyz.iinuwa.credentialsd.UiControl" +)] +trait FlowObject { + async fn start(&self) -> fdo::Result<()>; + async fn notify_state_changed(&self, event: BackgroundEvent) -> fdo::Result<()>; + + async fn cancel(&self) -> fdo::Result<()>; + + #[zbus(signal)] + async fn user_interacted(&self, update: BackendRequest) -> zbus::Result<()>; +} + #[derive(Debug)] pub struct UiControlServiceClient { conn: Connection, @@ -32,7 +125,23 @@ impl UiControlServiceClient { async fn proxy(&self) -> Result, zbus::Error> { UiControlServiceProxy::new(&self.conn).await } + + async fn proxy2(&self) -> Result, zbus::Error> { + UiControlService2Proxy::new(&self.conn).await + } + + async fn request_proxy( + &self, + request_id: RequestId, + ) -> Result, zbus::Error> { + let object_path = ObjectPath::from_string_unchecked(format!( + "/org/freedesktop/portal/Credential/{}", + request_id + )); + FlowObjectProxy::new(&self.conn, object_path).await + } } + impl UiController for UiControlServiceClient { async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { self.proxy() @@ -41,11 +150,73 @@ impl UiController for UiControlServiceClient { .await .map_err(|err| err.into()) } + + async fn initialize( + &self, + parent_window: Option, + origin: String, + r#type: Operation, + request_id: RequestId, + devices: Vec, + app_id: String, + app_display_name: String, + app_pid: u32, + app_path: String, + options: PortalBackendOptions, + ) -> Result> { + let path = self + .proxy2() + .await? + .initialize( + parent_window.into(), + origin, + r#type, + request_id, + devices, + app_id, + app_display_name, + app_pid, + app_path, + options, + ) + .await?; + tracing::debug!(?path, "Path initialized"); + let flow_object = FlowObjectProxy::new(&self.conn, path).await?; + let (from_ui_tx, from_ui_rx) = mpsc::channel(32); + let ui_event_stream = flow_object.receive_user_interacted().await?; + tokio::task::spawn(async move { + _ = forward_ui_events(ui_event_stream, from_ui_tx).await; + }); + // Mark as ready to receive messages. + flow_object.start().await?; + Ok(Flow { + proxy: Arc::new(flow_object), + ui_events_rx: Arc::new(AsyncMutex::new(from_ui_rx)), + }) + } +} + +async fn forward_ui_events( + mut ui_event_stream: UserInteractedStream, + tx: mpsc::Sender, +) -> Result<(), Box> { + tracing::debug!("Listening for events from UI"); + while let Some(signal) = ui_event_stream.next().await { + tracing::trace!(?signal, "Received event from UI"); + let event = signal.args()?.update; + if let Err(_) = tx.send(event).await { + tracing::trace!("credential service event listener stopped listening for UI events. Ending event stream listener"); + break; + } + } + tracing::trace!("Stopping UI event forwarder"); + Ok(()) } #[cfg(test)] pub mod test { use std::{ + error::Error, fmt::Debug, sync::{ atomic::{AtomicBool, Ordering}, @@ -55,7 +226,8 @@ pub mod test { use credentialsd_common::{ client::FlowController, - server::{BackgroundEvent, ViewRequest}, + model::{Device, Operation, PortalBackendOptions, RequestId}, + server::{BackgroundEvent, ViewRequest, WindowHandle}, }; use futures_lite::StreamExt; use tokio::sync::{ @@ -63,6 +235,8 @@ pub mod test { Mutex as AsyncMutex, Notify, }; + use crate::dbus::ui_control::Flow; + use super::UiController; #[derive(Debug)] @@ -71,7 +245,7 @@ pub mod test { } impl UiController for DummyUiClient { - async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { + async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { tracing::debug!( target: "DummyUiClient", "Sending launch_ui() request" @@ -83,6 +257,22 @@ pub mod test { ); Ok(()) } + + async fn initialize( + &self, + _parent_window: Option, + _origin: String, + _type: Operation, + _request_id: RequestId, + _devices: Vec, + _app_id: String, + _app_display_name: String, + _app_pid: u32, + _app_path: String, + _options: PortalBackendOptions, + ) -> Result> { + unimplemented!() + } } pub struct DummyUiServer @@ -212,7 +402,7 @@ pub mod test { ); } - async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { + async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { tracing::debug!( target: "DummyUiServer", "Received launch_ui() request" diff --git a/credentialsd/src/gateway/dbus.rs b/credentialsd/src/gateway/dbus.rs index 92a9a37..8652dea 100644 --- a/credentialsd/src/gateway/dbus.rs +++ b/credentialsd/src/gateway/dbus.rs @@ -292,7 +292,7 @@ impl CredentialPortalGateway { Err(err) => return Err(err).into(), }; - tracing::debug!( + tracing::trace!( ?context, %request_json, ?parent_window, diff --git a/credentialsd/src/gateway/mod.rs b/credentialsd/src/gateway/mod.rs index ce1b64a..26ca2cd 100644 --- a/credentialsd/src/gateway/mod.rs +++ b/credentialsd/src/gateway/mod.rs @@ -110,7 +110,7 @@ impl GatewayService { let response = self .request_controller - .request_credential(Some(context.into()), cred_request, parent_window) + .request_credential(context.into(), cred_request, parent_window) .await?; if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { @@ -165,7 +165,7 @@ impl GatewayService { let response = self .request_controller - .request_credential(Some(context.into()), cred_request, parent_window) + .request_credential(context.into(), cred_request, parent_window) .await?; if let CredentialResponse::GetPublicKeyCredentialResponse(cred_response) = response { diff --git a/credentialsd/src/gateway/util.rs b/credentialsd/src/gateway/util.rs index 57e1524..a367986 100644 --- a/credentialsd/src/gateway/util.rs +++ b/credentialsd/src/gateway/util.rs @@ -183,7 +183,8 @@ pub(super) fn create_credential_request_try_into_ctap2( .filter_map(|e| e.ok()) .collect() }); - let client_data_json = webauthn::format_client_data_json(Operation::Create, &challenge, origin); + let client_data_json = + webauthn::format_client_data_json(Operation::PublicKeyCreate, &challenge, origin); let client_data_hash = webauthn::create_client_data_hash(&client_data_json); Ok(( MakeCredentialRequest { @@ -283,7 +284,7 @@ pub(super) fn get_credential_request_try_into_ctap2( } let client_data_json = - webauthn::format_client_data_json(Operation::Get, &options.challenge, request_env); + webauthn::format_client_data_json(Operation::PublicKeyGet, &options.challenge, request_env); let client_data_hash = webauthn::create_client_data_hash(&client_data_json); // TODO: actually calculate correct effective domain, and use fallback to related origin requests to fill this in. For now, just default to origin. let user_verification = match options diff --git a/credentialsd/src/main.rs b/credentialsd/src/main.rs index ea920e8..4f0888e 100644 --- a/credentialsd/src/main.rs +++ b/credentialsd/src/main.rs @@ -36,9 +36,8 @@ async fn run() -> Result<(), Box> { let ui_controller = UiControlServiceClient::new(dbus_client_conn); let credential_service = CredentialService::new( InternalHybridHandler::new(), - InProcessUsbHandler {}, InProcessNfcHandler {}, - Arc::new(ui_controller), + InProcessUsbHandler {}, ); let (_flow_control_conn, initiator) = dbus::start_flow_control_service(credential_service).await?; diff --git a/credentialsd/src/webauthn.rs b/credentialsd/src/webauthn.rs index a539e82..2a0ba0e 100644 --- a/credentialsd/src/webauthn.rs +++ b/credentialsd/src/webauthn.rs @@ -680,8 +680,8 @@ pub fn format_client_data_json( origin: &NavigationContext, ) -> String { let op_str = match op { - Operation::Create => "webauthn.create", - Operation::Get => "webauthn.get", + Operation::PublicKeyCreate => "webauthn.create", + Operation::PublicKeyGet => "webauthn.get", }; let mut client_data_json = format!( r#"{{"type":"{}","challenge":"{}","origin":"{}""#, @@ -868,7 +868,7 @@ mod tests { fn test_same_origin_client_data_json_str() { let expected = r#"{"type":"webauthn.create","challenge":"abcd","origin":"https://example.com","crossOrigin":false}"#; let json = format_client_data_json( - Operation::Create, + Operation::PublicKeyCreate, "abcd", &NavigationContext::SameOrigin("https://example.com".parse().unwrap()), ); @@ -879,7 +879,7 @@ mod tests { fn test_cross_origin_client_data_json_str() { let expected = r#"{"type":"webauthn.create","challenge":"abcd","origin":"https://example.com","crossOrigin":true,"topOrigin":"https://example.org"}"#; let json = format_client_data_json( - Operation::Create, + Operation::PublicKeyCreate, "abcd", &NavigationContext::CrossOrigin(( "https://example.com".parse().unwrap(), diff --git a/doc/xyz.iinuwa.credentialsd.FlowControl.xml b/doc/xyz.iinuwa.credentialsd.FlowControl.xml new file mode 100644 index 0000000..1557b2f --- /dev/null +++ b/doc/xyz.iinuwa.credentialsd.FlowControl.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/xyz.iinuwa.credentialsd.UiControl.xml b/doc/xyz.iinuwa.credentialsd.UiControl.xml new file mode 100644 index 0000000..e2357b0 --- /dev/null +++ b/doc/xyz.iinuwa.credentialsd.UiControl.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meson.build b/meson.build index d9a9913..d075ed4 100644 --- a/meson.build +++ b/meson.build @@ -28,11 +28,16 @@ meson.add_dist_script( meson.project_source_root(), ) +# Libs and executables subdir('credentialsd-common') subdir('credentialsd') subdir('credentialsd-ui') + +# Data files +subdir('doc') subdir('dbus') +subdir('portal') subdir('systemd') subdir('webext') -subdir('doc') -subdir('demo_client') + +subdir('demo_client') \ No newline at end of file diff --git a/portal/credentialsd.portal b/portal/credentialsd.portal new file mode 100644 index 0000000..938145c --- /dev/null +++ b/portal/credentialsd.portal @@ -0,0 +1,3 @@ +[portal] +DBusName=xyz.iinuwa.credentialsd.UiControl +Interfaces=org.freedesktop.impl.portal.experimental.Credential diff --git a/portal/meson.build b/portal/meson.build new file mode 100644 index 0000000..ea237c5 --- /dev/null +++ b/portal/meson.build @@ -0,0 +1,4 @@ +install_data( + 'credentialsd.portal', + install_dir: datadir / 'xdg-desktop-portal/portals/', +) \ No newline at end of file