From b765e63745f78648ecef9cf56368437f3c737c71 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 16 Jul 2025 14:44:09 -0500 Subject: [PATCH 01/38] Update README --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 76502d0e..2dd6e8a0 100644 --- a/README.md +++ b/README.md @@ -83,19 +83,20 @@ first-party credential provider. Some non-goals: -- Fully implement the proposed specification. This repo is focused on defining -the D-Bus API for clients and portal frontend/backend implementations to use. -Though I would love to help implement, I don't think I will have the time to -fully implement the features specced by the API, so I welcome collaboration -from others to help implement. For now, any implementation in this repository -is for reference purposes. +- Fully integrate with any specific desktop environment. Each desktop +environment (GNOME, KDE, etc.) has its own UI and UX conventions, as well as +system configuration methods (e.g., GNOME Settings), which this API will need to integrate with. +Because of the variation, we intend to leave integration with these other +components to developers more familiar with each of the desktop environments. +For now, we are using bare GTK to build a UI for testing, but any UI +implementation in this repository is for reference purposes. If anyone is willing to do some of this integration work, feel free to contact us! - Create a full-featured password manager. Features like Password syncing, password generation, rotation, etc. is not part of this specficiation. Other password manager projects should be able to use this to make their credentials available to the user uniformly, though. -- BSD support. While I'd love to help out all open desktop environments, I don't +- BSD support. While we'd love to help out all open desktop environments, we don't know enough about any BSD to make it useful for them. Hopefully, the design process is transparent enough that someone else could design something that works for BSDs. @@ -106,8 +107,8 @@ works for BSDs. - March 2025: Integrated libwebauthn to support USB authenticators. - May 2024: Met with developers in GNOME and systemd to design internals for securely storing device credentials. -- Jan 2024: I've defined the [scenarios](doc/scenarios.md) that I expect this - API to cover. I am working on extracting [API methods](doc/api.md) required to +- Jan 2024: Defined the [scenarios](doc/scenarios.md) that we expect this + API to cover. We are working on extracting [API methods](doc/api.md) required to implement the interactions between the client, portal frontend, portal backend, machine and mobile devices. Once that is done, I intend to convert the API into a [portal spec](doc/design-doc.md), making it fit normal D-Bus/portal patterns. From d1c6657d400e47ae7ab90c42aa1e7b2406d213eb Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 16 Jul 2025 14:46:45 -0500 Subject: [PATCH 02/38] Remove bogus ViewEvent variant --- .../src/gui/view_model/gtk/mod.rs | 4 ---- .../src/gui/view_model/gtk/window.rs | 14 -------------- .../src/gui/view_model/mod.rs | 4 ---- 3 files changed, 22 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/mod.rs index d74a0f1f..16522b2a 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/mod.rs @@ -312,10 +312,6 @@ impl ViewModel { self.set_prompt("Multiple devices found. Please select with which to proceed."); } - pub async fn send_thingy(&self) { - self.send_event(ViewEvent::ButtonClicked).await; - } - pub async fn send_usb_device_pin(&self, pin: String) { self.send_event(ViewEvent::UsbPinEntered(pin)).await; } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/window.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/window.rs index 2db244ba..c1f41cb2 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/window.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/gtk/window.rs @@ -41,20 +41,6 @@ mod imp { #[gtk::template_callbacks] impl ExampleApplicationWindow { - #[template_callback] - fn handle_button_clicked(&self, _: >k::Button) { - println!("clicked"); - let view_model = &self.view_model.borrow(); - let view_model = view_model.as_ref().unwrap(); - glib::spawn_future_local(clone!( - #[weak] - view_model, - async move { - view_model.send_thingy().await; - } - )); - } - #[template_callback] fn handle_usb_pin_entered(&self, entry: >k::PasswordEntry) { let view_model = &self.view_model.borrow(); diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs index ed35dd4e..484704e6 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs @@ -168,9 +168,6 @@ impl ViewModel { self.update_title().await; self.update_devices().await; } - Event::View(ViewEvent::ButtonClicked) => { - println!("Got it!") - } Event::View(ViewEvent::DeviceSelected(id)) => { self.select_device(&id).await; println!("Selected device {id}"); @@ -302,7 +299,6 @@ impl ViewModel { pub enum ViewEvent { Initiated, - ButtonClicked, DeviceSelected(String), CredentialSelected(String), UsbPinEntered(String), From 7cbf77ed7f40a18d7001a4b5f8bd1433897bf385 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 17 Jul 2025 08:30:45 -0500 Subject: [PATCH 03/38] Move shared models from view_model to model --- .../src/credential_service/mod.rs | 2 +- .../src/credential_service/server.rs | 2 +- .../src/credential_service/usb.rs | 2 +- .../src/dbus.rs | 6 +- .../src/gui/mod.rs | 3 +- .../src/gui/view_model/mod.rs | 91 +------------------ .../src/main.rs | 1 + .../src/model.rs | 81 +++++++++++++++++ 8 files changed, 90 insertions(+), 98 deletions(-) create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/src/model.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 1ae7ffe6..997ccdf7 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 @@ -18,7 +18,7 @@ use libwebauthn::{ use crate::{ credential_service::{hybrid::HybridEvent, usb::UsbEvent}, dbus::{CredentialRequest, CredentialResponse}, - gui::view_model::{Device, Transport}, + model::{Device, Transport}, }; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; 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 ceab9ea1..9c24295b 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 @@ -7,7 +7,7 @@ use futures_lite::Stream; use tokio::sync::{mpsc, oneshot}; use crate::dbus::{CredentialRequest, CredentialResponse}; -use crate::gui::view_model::Device; +use crate::model::Device; use super::hybrid::{HybridHandler, HybridState}; use super::usb::{UsbHandler, UsbState}; 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 31c20dab..6240c40c 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 @@ -19,7 +19,7 @@ use tracing::{debug, warn}; use crate::{ dbus::{CredentialRequest, GetAssertionResponseInternal}, - gui::view_model::Credential, + model::Credential, }; use super::{AuthenticatorResponse, CredentialResponse, Error}; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 539d2a6d..cb2b5f16 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -25,10 +25,8 @@ use zbus::{ }; use crate::credential_service::CredentialManagementClient; -use crate::gui::{ - view_model::{CredentialType, Operation}, - ViewRequest, -}; +use crate::gui::ViewRequest; +use crate::model::{CredentialType, Operation}; use crate::webauthn::{ self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, }; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs index 69919b50..0959929a 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs @@ -6,8 +6,9 @@ use async_std::channel::Receiver; use tokio::sync::oneshot; use crate::credential_service::CredentialServiceClient; +use crate::model::Operation; -use view_model::{Operation, ViewEvent, ViewUpdate}; +use view_model::{ViewEvent, ViewUpdate}; pub struct ViewRequest { pub operation: Operation, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs index 484704e6..77542eda 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs @@ -13,6 +13,7 @@ use tracing::{error, info}; use crate::credential_service::{ CredentialServiceClient, Error as CredentialServiceError, UsbState, }; +use crate::model::{Credential, Device, Operation, Transport}; #[derive(Debug)] pub(crate) struct ViewModel @@ -334,30 +335,6 @@ pub enum Event { View(ViewEvent), } -#[derive(Clone, Debug, Default)] -pub struct Credential { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) username: Option, -} - -#[derive(Debug, Default)] -pub enum FingerprintSensorState { - #[default] - Idle, -} - -#[derive(Debug)] -pub enum CredentialType { - Passkey, - // Password, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Device { - pub id: String, - pub transport: Transport, -} #[derive(Clone, Debug, Default)] pub enum HybridState { @@ -400,69 +377,3 @@ impl From for HybridState { } } } - -#[derive(Debug)] -pub enum Operation { - Create { cred_type: CredentialType }, - Get { cred_types: Vec }, -} - -#[derive(Debug, Default)] -pub struct Provider; - -#[derive(Clone, Debug, PartialEq)] -pub enum Transport { - Ble, - HybridLinked, - HybridQr, - Internal, - Nfc, - Usb, -} - -impl TryInto for String { - type Error = String; - - fn try_into(self) -> Result { - let value: &str = self.as_ref(); - value.try_into() - } -} - -impl TryInto for &str { - type Error = String; - - fn try_into(self) -> Result { - match self { - "BLE" => Ok(Transport::Ble), - "HybridLinked" => Ok(Transport::HybridLinked), - "HybridQr" => Ok(Transport::HybridQr), - "Internal" => Ok(Transport::Internal), - "NFC" => Ok(Transport::Nfc), - "USB" => Ok(Transport::Usb), - _ => Err(format!("Unrecognized transport: {}", self.to_owned())), - } - } -} - -impl From for String { - fn from(val: Transport) -> Self { - val.as_str().to_string() - } -} - -impl Transport { - fn as_str(&self) -> &'static str { - match self { - Transport::Ble => "BLE", - Transport::HybridLinked => "HybridLinked", - Transport::HybridQr => "HybridQr", - Transport::Internal => "Internal", - Transport::Nfc => "NFC", - Transport::Usb => "USB", - } - } -} - -#[derive(Debug, Default)] -pub struct UserVerificationMethod; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index de43e1c0..eac64eb3 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -5,6 +5,7 @@ mod cose; mod credential_service; mod dbus; mod gui; +mod model; mod serde; mod webauthn; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs new file mode 100644 index 00000000..5ea92cb3 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs @@ -0,0 +1,81 @@ +use serde::{Deserialize, Serialize}; + + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Credential { + pub(crate) id: String, + pub(crate) name: String, + pub(crate) username: Option, +} + +#[derive(Debug)] +pub enum CredentialType { + Passkey, + // Password, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Device { + pub id: String, + pub transport: Transport, +} + +#[derive(Debug)] +pub enum Operation { + Create { cred_type: CredentialType }, + Get { cred_types: Vec }, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Transport { + Ble, + HybridLinked, + HybridQr, + Internal, + Nfc, + Usb, +} + +impl TryInto for String { + type Error = String; + + fn try_into(self) -> Result { + let value: &str = self.as_ref(); + value.try_into() + } +} + +impl TryInto for &str { + type Error = String; + + fn try_into(self) -> Result { + match self { + "BLE" => Ok(Transport::Ble), + "HybridLinked" => Ok(Transport::HybridLinked), + "HybridQr" => Ok(Transport::HybridQr), + "Internal" => Ok(Transport::Internal), + "NFC" => Ok(Transport::Nfc), + "USB" => Ok(Transport::Usb), + _ => Err(format!("Unrecognized transport: {}", self.to_owned())), + } + } +} + +impl From for String { + fn from(val: Transport) -> Self { + val.as_str().to_string() + } +} + +impl Transport { + pub fn as_str(&self) -> &'static str { + match self { + Transport::Ble => "BLE", + Transport::HybridLinked => "HybridLinked", + Transport::HybridQr => "HybridQr", + Transport::Internal => "Internal", + Transport::Nfc => "NFC", + Transport::Usb => "USB", + } + } +} From 055cfee24bf25b8157ccbc609c732fc01a34e167 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 17 Jul 2025 08:30:45 -0500 Subject: [PATCH 04/38] Add D-Bus methods and serialization for decoupling UI from credential service --- .../src/dbus.rs | 162 ++++++++++++++++++ .../src/gui/view_model/mod.rs | 3 + 2 files changed, 165 insertions(+) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index cb2b5f16..32a77ce8 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -16,7 +16,10 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialUserEntity, }; use ring::digest; +use serde::{Deserialize, Serialize}; use tokio::sync::Mutex as AsyncMutex; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedValue, Value, LE}; use zbus::{ connection::{self, Connection}, fdo, interface, @@ -25,6 +28,7 @@ use zbus::{ }; use crate::credential_service::CredentialManagementClient; +use crate::gui::view_model::ViewUpdate; use crate::gui::ViewRequest; use crate::model::{CredentialType, Operation}; use crate::webauthn::{ @@ -187,8 +191,35 @@ impl CredentialManager signal_unknown_credential: false, }) } + + async fn initiate_event_stream(&self) -> fdo::Result<()> { + todo!() + } + async fn select_device(&self, device_id: String) -> fdo::Result<()> { + todo!() + } + async fn enter_client_pin(&self, pin: String) -> fdo::Result<()> { + todo!() + } + async fn select_credential(&self, credential_id: String) -> fdo::Result<()> { + todo!() + } + + async fn send_state_update( + &self, + #[zbus(signal_emitter)] + emitter: SignalEmitter<'_>, + update: ClientUpdate, + ) -> fdo::Result<()> { + emitter.state_changed(update).await?; + Ok(()) + } + + #[zbus(signal)] + async fn state_changed(emitter: &SignalEmitter<'_>, update: ClientUpdate) -> zbus::Result<()>; } + async fn execute_flow( gui_tx: &async_std::channel::Sender, manager_client: &C, @@ -768,6 +799,137 @@ pub struct GetClientCapabilitiesResponse { signal_unknown_credential: bool, } +/// Updates to send to the client +#[derive(Serialize, Deserialize, Type)] +pub enum ClientUpdate { + SetTitle(OwnedValue), + SetDevices(OwnedValue), + SetCredentials(OwnedValue), + + WaitingForDevice(OwnedValue), + SelectingDevice(OwnedValue), + + UsbNeedsPin(OwnedValue), + UsbNeedsUserVerification(OwnedValue), + UsbNeedsUserPresence(OwnedValue), + + HybridNeedsQrCode(OwnedValue), + HybridConnecting(OwnedValue), + HybridConnected(OwnedValue), + + Completed(OwnedValue), + Failed(OwnedValue), +} + +impl TryFrom for ViewUpdate { + type Error = zbus::zvariant::Error; + fn try_from(value: ClientUpdate) -> std::result::Result { + match value { + ClientUpdate::SetTitle(v) => v.try_into().map(|title| Self::SetTitle(title)), + ClientUpdate::SetDevices(v) => { + let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; + let devices: std::result::Result, zbus::zvariant::Error> = dbus_devices + .into_iter() + .map(|d| d.try_into() + .map_err(|_| zbus::zvariant::Error::Message("Could not deserialize devices".to_string())) + ) + .collect(); + Ok(Self::SetDevices(devices?)) + }, + ClientUpdate::SetCredentials(v) => { + let dbus_credentials: Vec = Value::<'_>::from(v).try_into()?; + let credentials: std::result::Result, zbus::zvariant::Error> = dbus_credentials + .into_iter() + .map(|creds| creds.try_into() + .map_err(|_| zbus::zvariant::Error::Message("Could not deserialize credentials".to_string())) + ) + .collect(); + Ok(Self::SetCredentials(credentials?)) + }, + + ClientUpdate::WaitingForDevice(v) => { + let dbus_device: Device = Value::<'_>::from(v).try_into()?; + let device: crate::model::Device = dbus_device + .try_into() + .map_err(|_| zbus::zvariant::Error::Message("Could not deserialize device".to_string()))?; + Ok(Self::WaitingForDevice(device)) + }, + ClientUpdate::SelectingDevice(_) => Ok(Self::SelectingDevice), + + ClientUpdate::UsbNeedsPin(v) => v.try_into().map(|x: i32| { + let attempts_left = if x == -1 { None } else { Some(x as u32) }; + Self::UsbNeedsPin { attempts_left } + }), + ClientUpdate::UsbNeedsUserVerification(v) => v.try_into().map(|x: i32| { + let attempts_left = if x == -1 { None } else { Some(x as u32) }; + Self::UsbNeedsUserVerification { attempts_left } + }), + ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), + + ClientUpdate::HybridNeedsQrCode(v) => v.try_into().map(|qr_code_data| Self::HybridNeedsQrCode(qr_code_data)), + ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), + ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), + + ClientUpdate::Completed(_) => Ok(Self::Completed), + ClientUpdate::Failed(v) => v.try_into().map(|error_msg| Self::Failed(error_msg)), + } + } +} + +#[derive(SerializeDict, DeserializeDict, Type)] +struct Credential { + id: String, + name: String, + username: String, +} + +impl From for crate::model::Credential { + fn from(value: Credential) -> Self { + Self { + id: value.id, + name: value.name, + username: if value.username.is_empty() { None } else { Some(value.username) } + } + } +} + +impl TryFrom> for Credential { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let credential: Credential = encoded.deserialize()?.0; + Ok(credential) + } +} + +#[derive(SerializeDict, DeserializeDict, Type)] +struct Device { + id: String, + transport: String, +} + +impl TryFrom> for Device { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let device: Device = encoded.deserialize()?.0; + Ok(device) + } +} + +impl TryFrom for crate::model::Device { + type Error = (); + fn try_from(value: Device) -> std::result::Result { + let transport = value.transport.try_into().map_err(|_| ())?; + Ok(Self { + id: value.id, + transport, + }) + } +} + fn format_client_data_json( op: Operation, challenge: &str, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs index 77542eda..16e92c47 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs @@ -7,6 +7,7 @@ use async_std::{ channel::{Receiver, Sender}, sync::Mutex, }; +use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{error, info}; @@ -298,6 +299,7 @@ impl ViewModel { } } +#[derive(Serialize, Deserialize)] pub enum ViewEvent { Initiated, DeviceSelected(String), @@ -305,6 +307,7 @@ pub enum ViewEvent { UsbPinEntered(String), } +#[derive(Serialize, Deserialize)] pub enum ViewUpdate { SetTitle(String), SetDevices(Vec), From f11c0a3281dc7897983b00b3dbf783641bafa8c8 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 19 Jul 2025 21:39:30 -0500 Subject: [PATCH 05/38] Advertise support for hybrid transport in client capabilities --- xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 32a77ce8..3478d6da 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -182,7 +182,7 @@ impl CredentialManager Ok(GetClientCapabilitiesResponse { conditional_create: false, conditional_get: false, - hybrid_transport: false, + hybrid_transport: true, passkey_platform_authenticator: false, user_verifying_platform_authenticator: false, related_origins: false, From c2a3ac3f966250dcf9fb57bee37341fee62e9ed2 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 19 Jul 2025 21:39:30 -0500 Subject: [PATCH 06/38] Do a whole bunch of stuff to move background events to credential client --- .../src/credential_service/hybrid.rs | 8 +- .../src/credential_service/mod.rs | 17 -- .../src/credential_service/server.rs | 130 ++++++++++-- .../src/credential_service/usb.rs | 34 ++- .../src/dbus.rs | 194 +++++++++++++++--- .../src/gui/mod.rs | 39 +++- .../src/gui/view_model/mod.rs | 119 ++--------- .../src/model.rs | 115 ++++++++++- 8 files changed, 483 insertions(+), 173 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 8c8a24a2..8750fc58 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,11 +1,10 @@ use core::panic; use std::fmt::Debug; -use crate::dbus::CredentialRequest; use async_stream::stream; use futures_lite::Stream; use tokio::sync::broadcast; -use tokio::sync::mpsc::{self, Receiver, Sender}; +use tokio::sync::mpsc::{self, Sender}; use tracing::{debug, error}; use libwebauthn::transport::cable::channel::{CableUpdate, CableUxUpdate}; @@ -13,7 +12,10 @@ use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOpe use libwebauthn::transport::{Channel, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; -use super::{AuthenticatorResponse, Error}; +use crate::dbus::CredentialRequest; +use crate::model::Error; + +use super::AuthenticatorResponse; pub(crate) trait HybridHandler { fn start( 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 997ccdf7..5f8d1f66 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 @@ -194,23 +194,6 @@ enum AuthenticatorResponse { CredentialsAsserted(GetAssertionResponse), } -#[derive(Debug, Clone)] -pub enum Error { - /// Some unknown error with the authenticator occurred. - AuthenticatorError, - /// No matching credentials were found on the device. - NoCredentials, - /// Too many incorrect PIN attempts, and authenticator must be removed and - /// reinserted to continue any more PIN attempts. - /// - /// Note that this is different than exhausting the PIN count that fully - /// locks out the device. - PinAttemptsExhausted, - // TODO: We may want to hide the details on this variant from the public API. - /// Something went wrong with the credential service itself, not the authenticator. - Internal(String), -} - impl From for AuthenticatorResponse { fn from(value: MakeCredentialResponse) -> Self { Self::CredentialCreated(value) 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 9c24295b..2faabf3a 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 @@ -3,11 +3,11 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use futures_lite::Stream; +use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot}; use crate::dbus::{CredentialRequest, CredentialResponse}; -use crate::model::Device; +use crate::model::{BackgroundEvent, Device}; use super::hybrid::{HybridHandler, HybridState}; use super::usb::{UsbHandler, UsbState}; @@ -74,10 +74,20 @@ pub trait CredentialServiceClient { fn get_hybrid_credential( &self, - ) -> impl Future + Send>>> + Send; + // ) -> impl Future + Send>>> + Send; + ) -> impl Future> + Send; fn get_usb_credential( + &mut self, + // ) -> impl Future + Send>>> + Send; + ) -> impl Future> + Send; + fn initiate_event_stream( + &mut self, + ) -> impl Future + Send + '_>>, ()>>; + fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; + fn select_credential( &self, - ) -> impl Future + Send>>> + Send; + credential_id: String, + ) -> impl Future> + Send; } pub trait CredentialManagementClient { @@ -144,6 +154,9 @@ pub struct InProcessClient { InProcessServerRequest, oneshot::Sender, )>, + bg_event_tx: Option>, + bg_event_rx: Option>, + usb_pin_tx: Option>, } impl InProcessClient { @@ -177,26 +190,85 @@ impl CredentialServiceClient for InProcessClient { } } - async fn get_hybrid_credential(&self) -> Pin + Send>> { + async fn get_hybrid_credential(&self) -> Result<(), ()> { let response = self .send(ServiceRequest::GetHybridCredential) .await .unwrap(); - if let ServiceResponse::GetHybridCredential(stream) = response { - stream + if let ServiceResponse::GetHybridCredential(mut stream) = response { + if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { + while let Some(hybrid_state) = stream.next().await { + if let Some(tx) = tx_weak.upgrade() { + match hybrid_state { + HybridState::Completed | HybridState::Failed => { + tx.send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + .await + .unwrap(); + break; + } + _ => tx + .send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + .await + .unwrap(), + }; + } + } + }; + Ok(()) } else { panic!("Unable to get hybrid credential"); } } - async fn get_usb_credential(&self) -> Pin + Send>> { + async fn get_usb_credential(&mut self) -> Result<(), ()> { let response = self.send(ServiceRequest::GetUsbCredential).await.unwrap(); - if let ServiceResponse::GetUsbCredential(stream) = response { - stream + if let ServiceResponse::GetUsbCredential(mut stream) = response { + if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { + while let Some(state) = stream.next().await { + if let Some(tx) = tx_weak.upgrade() { + tx.send(BackgroundEvent::UsbStateChanged((&state).into())) + .await + .unwrap(); + match state { + UsbState::NeedsPin { pin_tx, .. } => { + let _ = self.usb_pin_tx.insert(pin_tx); + } + UsbState::Completed | UsbState::Failed(_) => { + break; + } + _ => {} + }; + } + } + }; + Ok(()) } else { panic!("Unable to get usb credential"); } } + + async fn initiate_event_stream( + &mut self, + ) -> Result + Send + '_>>, ()> { + Ok(Box::pin(async_stream::stream! { + while let Some(ref mut rx) = self.bg_event_rx { + while let Some(bg_event) = rx.recv().await { + yield bg_event + } + } + })) + } + + async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { + if let Some(pin_tx) = self.usb_pin_tx.take() { + pin_tx.send(pin).await.unwrap(); + } + Ok(()) + } + + async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + todo!(); + } } impl CredentialServiceClient for Arc { @@ -204,16 +276,33 @@ impl CredentialServiceClient for Arc { InProcessClient::get_available_public_key_devices(self) } - fn get_hybrid_credential( - &self, - ) -> impl Future + Send>>> { + fn get_hybrid_credential(&self) -> impl Future> { + // ) -> impl Future + Send>>> { InProcessClient::get_hybrid_credential(self) } - fn get_usb_credential( - &self, - ) -> impl Future + Send>>> { - InProcessClient::get_usb_credential(self) + async fn get_usb_credential( + &mut self, + // ) -> impl Future + Send>>> { + ) -> Result<(), ()> { + let client = Arc::get_mut(self).ok_or(())?; + InProcessClient::get_usb_credential(client).await + } + + async fn initiate_event_stream( + &mut self, + ) -> Result + Send + '_>>, ()> { + let client = Arc::get_mut(self).ok_or(())?; + InProcessClient::initiate_event_stream(client).await + } + + async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { + let client = Arc::get_mut(self).ok_or(())?; + InProcessClient::enter_client_pin(client, pin).await + } + + fn select_credential(&self, credential_id: String) -> impl Future> { + InProcessClient::select_credential(self, credential_id) } } @@ -241,7 +330,12 @@ where let mgr_tx = tx.clone(); let mgr = InProcessManager { tx: mgr_tx }; let client_tx = tx.clone(); - let client = InProcessClient { tx: client_tx }; + let client = InProcessClient { + tx: client_tx, + bg_event_tx: None, + bg_event_rx: None, + usb_pin_tx: None, + }; (Self { svc, rx }, mgr, client) } 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 6240c40c..309faaec 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 @@ -19,10 +19,10 @@ use tracing::{debug, warn}; use crate::{ dbus::{CredentialRequest, GetAssertionResponseInternal}, - model::Credential, + model::{Credential, Error}, }; -use super::{AuthenticatorResponse, CredentialResponse, Error}; +use super::{AuthenticatorResponse, CredentialResponse}; pub(crate) trait UsbHandler { fn start( @@ -527,6 +527,36 @@ impl From for UsbState { } } +impl From for crate::model::UsbState { + fn from(value: UsbState) -> Self { + Self::from(&value) + } +} +impl From<&UsbState> for crate::model::UsbState { + fn from(value: &UsbState) -> Self { + match value { + UsbState::Idle => crate::model::UsbState::Idle, + UsbState::Waiting => crate::model::UsbState::Waiting, + UsbState::SelectingDevice => crate::model::UsbState::SelectingDevice, + UsbState::Connected => crate::model::UsbState::Connected, + UsbState::NeedsPin { attempts_left, .. } => crate::model::UsbState::NeedsPin { + attempts_left: *attempts_left, + }, + UsbState::NeedsUserVerification { attempts_left } => { + crate::model::UsbState::NeedsUserVerification { + attempts_left: *attempts_left, + } + } + UsbState::NeedsUserPresence => crate::model::UsbState::NeedsUserPresence, + UsbState::SelectCredential { creds, .. } => crate::model::UsbState::SelectCredential { + creds: creds.to_owned(), + }, + UsbState::Completed => crate::model::UsbState::Completed, + UsbState::Failed(err) => crate::model::UsbState::Failed(err.to_owned()), + } + } +} + async fn handle_usb_updates( signal_tx: &WeakSender>, mut state_rx: broadcast::Receiver, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 3478d6da..5943a91d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::Duration; @@ -28,9 +28,8 @@ use zbus::{ }; use crate::credential_service::CredentialManagementClient; -use crate::gui::view_model::ViewUpdate; use crate::gui::ViewRequest; -use crate::model::{CredentialType, Operation}; +use crate::model::{CredentialType, Operation, ViewUpdate}; use crate::webauthn::{ self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, }; @@ -50,15 +49,27 @@ pub(crate) async fn start_service), + /// Client is actively receiving messages. + Active, +} + struct CredentialManager { app_lock: Arc>>, manager_client: C, + signal_state: Arc>, } #[interface(name = "xyz.iinuwa.credentials.CredentialManagerUi1")] @@ -192,8 +203,22 @@ impl CredentialManager }) } - async fn initiate_event_stream(&self) -> fdo::Result<()> { - todo!() + async fn initiate_event_stream( + &self, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> fdo::Result<()> { + let mut signal_state = self.signal_state.lock().await; + match *signal_state { + SignalState::Idle => {} + SignalState::Pending(ref mut pending) => { + for msg in pending.into_iter() { + emitter.state_changed(msg).await?; + } + } + SignalState::Active => {} + }; + *signal_state = SignalState::Active; + Ok(()) } async fn select_device(&self, device_id: String) -> fdo::Result<()> { todo!() @@ -207,19 +232,32 @@ impl CredentialManager async fn send_state_update( &self, - #[zbus(signal_emitter)] - emitter: SignalEmitter<'_>, - update: ClientUpdate, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + update: BackgroundEvent, ) -> fdo::Result<()> { - emitter.state_changed(update).await?; + let mut signal_state = self.signal_state.lock().await; + match *signal_state { + SignalState::Idle => { + let pending = VecDeque::from([update]); + *signal_state = SignalState::Pending(pending); + } + SignalState::Pending(ref mut pending) => { + pending.push_back(update); + } + SignalState::Active => { + emitter.state_changed(&update).await?; + } + }; Ok(()) } #[zbus(signal)] - async fn state_changed(emitter: &SignalEmitter<'_>, update: ClientUpdate) -> zbus::Result<()>; + async fn state_changed( + emitter: &SignalEmitter<'_>, + update: &BackgroundEvent, + ) -> zbus::Result<()>; } - async fn execute_flow( gui_tx: &async_std::channel::Sender, manager_client: &C, @@ -828,32 +866,44 @@ impl TryFrom for ViewUpdate { ClientUpdate::SetTitle(v) => v.try_into().map(|title| Self::SetTitle(title)), ClientUpdate::SetDevices(v) => { let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; - let devices: std::result::Result, zbus::zvariant::Error> = dbus_devices - .into_iter() - .map(|d| d.try_into() - .map_err(|_| zbus::zvariant::Error::Message("Could not deserialize devices".to_string())) - ) - .collect(); + let devices: std::result::Result, zbus::zvariant::Error> = + dbus_devices + .into_iter() + .map(|d| { + d.try_into().map_err(|_| { + zbus::zvariant::Error::Message( + "Could not deserialize devices".to_string(), + ) + }) + }) + .collect(); Ok(Self::SetDevices(devices?)) - }, + } ClientUpdate::SetCredentials(v) => { let dbus_credentials: Vec = Value::<'_>::from(v).try_into()?; - let credentials: std::result::Result, zbus::zvariant::Error> = dbus_credentials + let credentials: std::result::Result< + Vec, + zbus::zvariant::Error, + > = dbus_credentials .into_iter() - .map(|creds| creds.try_into() - .map_err(|_| zbus::zvariant::Error::Message("Could not deserialize credentials".to_string())) - ) + .map(|creds| { + creds.try_into().map_err(|_| { + zbus::zvariant::Error::Message( + "Could not deserialize credentials".to_string(), + ) + }) + }) .collect(); Ok(Self::SetCredentials(credentials?)) - }, + } ClientUpdate::WaitingForDevice(v) => { let dbus_device: Device = Value::<'_>::from(v).try_into()?; - let device: crate::model::Device = dbus_device - .try_into() - .map_err(|_| zbus::zvariant::Error::Message("Could not deserialize device".to_string()))?; + let device: crate::model::Device = dbus_device.try_into().map_err(|_| { + zbus::zvariant::Error::Message("Could not deserialize device".to_string()) + })?; Ok(Self::WaitingForDevice(device)) - }, + } ClientUpdate::SelectingDevice(_) => Ok(Self::SelectingDevice), ClientUpdate::UsbNeedsPin(v) => v.try_into().map(|x: i32| { @@ -866,7 +916,9 @@ impl TryFrom for ViewUpdate { }), ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), - ClientUpdate::HybridNeedsQrCode(v) => v.try_into().map(|qr_code_data| Self::HybridNeedsQrCode(qr_code_data)), + ClientUpdate::HybridNeedsQrCode(v) => v + .try_into() + .map(|qr_code_data| Self::HybridNeedsQrCode(qr_code_data)), ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), @@ -888,7 +940,11 @@ impl From for crate::model::Credential { Self { id: value.id, name: value.name, - username: if value.username.is_empty() { None } else { Some(value.username) } + username: if value.username.is_empty() { + None + } else { + Some(value.username) + }, } } } @@ -930,6 +986,86 @@ impl TryFrom for crate::model::Device { } } +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +enum HybridState { + /// Default state, not listening for hybrid transport. + Idle(OwnedValue), + + /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. + Started(OwnedValue), + + /// BLE advert received, connecting to caBLE tunnel with shared secret. + Connecting(OwnedValue), + + /// Connected to device via caBLE tunnel. + Connected(OwnedValue), + + /// Credential received over tunnel. + Completed(OwnedValue), + + // This isn't actually sent from the server. + UserCancelled(OwnedValue), + + /// Failed to receive a credential + Failed(OwnedValue), +} + +impl TryFrom for crate::model::HybridState { + type Error = zbus::zvariant::Error; + fn try_from(value: HybridState) -> std::result::Result { + match value { + HybridState::Idle(_) => Ok(Self::Idle), + HybridState::Started(value) => value.try_into().map(Self::Started), + HybridState::Connecting(_) => Ok(Self::Connecting), + HybridState::Connected(_) => Ok(Self::Connected), + HybridState::Completed(_) => Ok(Self::Completed), + HybridState::UserCancelled(_) => Ok(Self::UserCancelled), + HybridState::Failed(_) => Ok(Self::Failed), + } + } +} + +/// Used to de-/serialize state D-Bus and model::UsbState. +#[derive(Serialize, Deserialize, Type)] +enum UsbState { + Idle(OwnedValue), + Waiting(OwnedValue), + SelectingDevice(OwnedValue), + Connected(OwnedValue), + NeedsPin(OwnedValue), /* { + attempts_left: Option, + }, + */ + NeedsUserVerification(OwnedValue), /* { + attempts_left: Option, + },*/ + + NeedsUserPresence(OwnedValue), + //UserCancelled, + SelectCredential(OwnedValue), /* { + creds: Vec, + },*/ + Completed(OwnedValue), + // Failed(crate::credential_service::Error), + Failed(OwnedValue), +} + +#[derive(Serialize, Deserialize, Type)] +enum BackgroundEvent { + UsbStateChanged(OwnedValue), + HybridStateChanged(OwnedValue), +} + +impl TryFrom> for UsbState { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} + fn format_client_data_json( op: Operation, challenge: &str, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs index 0959929a..be1baf74 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs @@ -6,9 +6,9 @@ use async_std::channel::Receiver; use tokio::sync::oneshot; use crate::credential_service::CredentialServiceClient; -use crate::model::Operation; +use crate::model::{Operation, ViewUpdate}; -use view_model::{ViewEvent, ViewUpdate}; +use view_model::ViewEvent; pub struct ViewRequest { pub operation: Operation, @@ -48,3 +48,38 @@ fn run_gui(client: C, reques async_std::task::block_on(event_loop.cancel()); response_tx.send(()).unwrap(); } + +trait GuiClient { + /// Mark the GUI as ready to receive events from credential service. + /// Returns a queue of updates for updating the GUI state. + async fn initiate_event_stream(&self) -> Result, ()>; + + /// Select a authenticator or transport to interact with a credential. + async fn select_device(&self, device_id: String) -> Result<(), ()>; + + /// Send the client PIN to an authenticator. + async fn enter_client_pin(&self, pin: String) -> Result<(), ()>; + + /// Confirm user's credential selection when an authenticator returns multiple credentials. + async fn select_credential(&self, credential_id: String) -> Result<(), ()>; +} + +struct InProcessGuiClient {} + +impl GuiClient for InProcessGuiClient { + async fn initiate_event_stream(&self) -> Result, ()> { + todo!() + } + + async fn select_device(&self, device_id: String) -> Result<(), ()> { + todo!() + } + + async fn enter_client_pin(&self, pin: String) -> Result<(), ()> { + todo!() + } + + async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + todo!() + } +} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs index 16e92c47..8706847a 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs @@ -11,10 +11,11 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{error, info}; -use crate::credential_service::{ - CredentialServiceClient, Error as CredentialServiceError, UsbState, +use crate::credential_service::CredentialServiceClient; +use crate::model::{ + BackgroundEvent, Credential, Device, Error, HybridState, Operation, Transport, UsbState, + ViewUpdate, }; -use crate::model::{Credential, Device, Operation, Transport}; #[derive(Debug)] pub(crate) struct ViewModel @@ -24,7 +25,6 @@ where credential_service: Arc>, tx_update: Sender, rx_event: Receiver, - bg_update: Sender, bg_event: Receiver, title: String, operation: Operation, @@ -34,7 +34,6 @@ where selected_device: Option, // providers: Vec, - usb_pin_tx: Option>>>, usb_cred_tx: Option>>>, hybrid_qr_state: HybridState, @@ -54,13 +53,11 @@ impl ViewModel { credential_service: Arc::new(Mutex::new(credential_service)), rx_event, tx_update, - bg_update, bg_event, operation, title: String::default(), devices: Vec::new(), selected_device: None, - usb_pin_tx: None, usb_cred_tx: None, hybrid_qr_state: HybridState::default(), hybrid_qr_code_data: None, @@ -96,7 +93,7 @@ impl ViewModel { pub(crate) async fn select_device(&mut self, id: &str) { let device = self.devices.iter().find(|d| d.id == id).unwrap(); - println!("{:?}", device); + tracing::debug!("Device selected: {:?}", device); // Handle previous device if let Some(prev_device) = self.selected_device.replace(device.clone()) { @@ -119,35 +116,12 @@ impl ViewModel { // start discovery for newly selected device match device.transport { 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().await - }; - async_std::task::spawn(async move { - // TODO: add cancellation - while let Some(usb_state) = stream.next().await { - // forward to background event loop - tx.send(BackgroundEvent::UsbStateChanged(usb_state)) - .await - .unwrap(); - } - }); + let mut cred_service = self.credential_service.lock().await; + cred_service.get_usb_credential().await.unwrap(); } 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().await; - async_std::task::spawn(async move { - while let Some(state) = stream.next().await { - // forward to background event loop - tx.send(BackgroundEvent::HybridQrStateChanged(state.into())) - .await - .unwrap(); - } - tracing::debug!("Broke out of hybrid QR state stream"); - }); + let mut cred_service = self.credential_service.lock().await; + cred_service.get_hybrid_credential().await.unwrap(); } _ => { todo!() @@ -175,10 +149,10 @@ impl ViewModel { println!("Selected device {id}"); } Event::View(ViewEvent::UsbPinEntered(pin)) => { - if let Some(pin_tx) = self.usb_pin_tx.take() { - if pin_tx.lock().await.send(pin).await.is_err() { - error!("Failed to send pin to device"); - } + let cred_service = self.credential_service.clone(); + let mut cred_service = cred_service.lock().await; + if cred_service.enter_client_pin(pin).await.is_err() { + error!("Failed to send pin to device"); } } Event::View(ViewEvent::CredentialSelected(cred_id)) => { @@ -200,11 +174,7 @@ impl ViewModel { info!("Found USB device") } - UsbState::NeedsPin { - attempts_left, - pin_tx, - } => { - let _ = self.usb_pin_tx.insert(Arc::new(Mutex::new(pin_tx))); + UsbState::NeedsPin { attempts_left } => { self.tx_update .send(ViewUpdate::UsbNeedsPin { attempts_left }) .await @@ -232,8 +202,7 @@ impl ViewModel { .unwrap(); } UsbState::Idle | UsbState::Waiting => {} - UsbState::SelectCredential { creds, cred_tx } => { - let _ = self.usb_cred_tx.insert(Arc::new(Mutex::new(cred_tx))); + UsbState::SelectCredential { creds } => { self.tx_update .send(ViewUpdate::SetCredentials(creds)) .await @@ -242,9 +211,9 @@ impl ViewModel { // TODO: Provide more specific error messages using the wrapped Error. UsbState::Failed(err) => { let error_msg = String::from(match err { - CredentialServiceError::NoCredentials => "No matching credentials found on this authenticator.", - CredentialServiceError::PinAttemptsExhausted => "No more PIN attempts allowed. Try removing your device and plugging it back in.", - CredentialServiceError::AuthenticatorError | CredentialServiceError::Internal(_) => "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", + Error::NoCredentials => "No matching credentials found on this authenticator.", + Error::PinAttemptsExhausted => "No more PIN attempts allowed. Try removing your device and plugging it back in.", + Error::AuthenticatorError | Error::Internal(_) => "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", }); self.tx_update .send(ViewUpdate::Failed(error_msg)) @@ -307,63 +276,11 @@ pub enum ViewEvent { UsbPinEntered(String), } -#[derive(Serialize, Deserialize)] -pub enum ViewUpdate { - SetTitle(String), - SetDevices(Vec), - SetCredentials(Vec), - - WaitingForDevice(Device), - SelectingDevice, - - UsbNeedsPin { attempts_left: Option }, - UsbNeedsUserVerification { attempts_left: Option }, - UsbNeedsUserPresence, - - HybridNeedsQrCode(String), - HybridConnecting, - HybridConnected, - - Completed, - Failed(String), -} - -pub enum BackgroundEvent { - UsbStateChanged(UsbState), - HybridQrStateChanged(HybridState), -} - pub enum Event { Background(BackgroundEvent), View(ViewEvent), } - -#[derive(Clone, Debug, Default)] -pub enum HybridState { - /// Default state, not listening for hybrid transport. - #[default] - Idle, - - /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. - Started(String), - - /// BLE advert received, connecting to caBLE tunnel with shared secret. - Connecting, - - /// Connected to device via caBLE tunnel. - Connected, - - /// Credential received over tunnel. - Completed, - - // This isn't actually sent from the server. - UserCancelled, - - /// Failed to receive a credential - Failed, -} - impl From for HybridState { fn from(value: crate::credential_service::hybrid::HybridState) -> Self { match value { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs index 5ea92cb3..1d57a59d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; - #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Credential { pub(crate) id: String, @@ -79,3 +78,117 @@ impl Transport { } } } + +#[derive(Serialize, Deserialize)] +pub enum ViewUpdate { + SetTitle(String), + SetDevices(Vec), + SetCredentials(Vec), + + WaitingForDevice(Device), + SelectingDevice, + + UsbNeedsPin { attempts_left: Option }, + UsbNeedsUserVerification { attempts_left: Option }, + UsbNeedsUserPresence, + + HybridNeedsQrCode(String), + HybridConnecting, + HybridConnected, + + Completed, + Failed(String), +} + +#[derive(Clone, Debug, Default)] +pub enum HybridState { + /// Default state, not listening for hybrid transport. + #[default] + Idle, + + /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. + Started(String), + + /// BLE advert received, connecting to caBLE tunnel with shared secret. + Connecting, + + /// Connected to device via caBLE tunnel. + Connected, + + /// Credential received over tunnel. + Completed, + + // This isn't actually sent from the server. + UserCancelled, + + /// Failed to receive a credential + Failed, +} + +/// Used to share public state between credential service and UI. +#[derive(Clone, Debug, Default)] +pub enum UsbState { + /// Not polling for FIDO USB device. + #[default] + Idle, + + /// Awaiting FIDO USB device to be plugged in. + Waiting, + + // When we encounter multiple devices, we let all of them blink and continue + // with the one that was tapped. + SelectingDevice, + + /// USB device connected, prompt user to tap + Connected, + + /// 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, + // TODO: implement cancellation + // This isn't actually sent from the server. + //UserCancelled, + + // Multiple credentials have been found and the user has to select which to use + // List of user-identities to decide which to use. + SelectCredential { + creds: Vec, + }, + + /// USB tapped, received credential + Completed, + + /// Interaction with the authenticator failed. + Failed(Error), +} + +pub enum BackgroundEvent { + UsbStateChanged(UsbState), + HybridQrStateChanged(HybridState), +} + +#[derive(Debug, Clone)] +pub enum Error { + /// Some unknown error with the authenticator occurred. + AuthenticatorError, + /// No matching credentials were found on the device. + NoCredentials, + /// Too many incorrect PIN attempts, and authenticator must be removed and + /// reinserted to continue any more PIN attempts. + /// + /// Note that this is different than exhausting the PIN count that fully + /// locks out the device. + PinAttemptsExhausted, + // TODO: We may want to hide the details on this variant from the public API. + /// Something went wrong with the credential service itself, not the authenticator. + Internal(String), +} From f4142b8e527188b0b864be527cae91b79e47c889 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 22 Jul 2025 10:17:19 -0500 Subject: [PATCH 07/38] Send background events to UI from spawned tasks --- .../src/credential_service/server.rs | 123 +++++++++++------- .../src/gui/mod.rs | 6 +- .../src/gui/view_model/mod.rs | 22 ++-- 3 files changed, 92 insertions(+), 59 deletions(-) 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 2faabf3a..9b8b1792 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 @@ -3,6 +3,7 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use async_std::sync::Mutex as AsyncMutex; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot}; @@ -73,16 +74,14 @@ pub trait CredentialServiceClient { ) -> impl Future, ()>> + Send; fn get_hybrid_credential( - &self, - // ) -> impl Future + Send>>> + Send; + &mut self, ) -> impl Future> + Send; fn get_usb_credential( &mut self, - // ) -> impl Future + Send>>> + Send; ) -> impl Future> + Send; fn initiate_event_stream( &mut self, - ) -> impl Future + Send + '_>>, ()>>; + ) -> impl Future + Send + 'static>>, ()>> + Send; fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; fn select_credential( &self, @@ -155,8 +154,21 @@ pub struct InProcessClient { oneshot::Sender, )>, bg_event_tx: Option>, - bg_event_rx: Option>, - usb_pin_tx: Option>, + usb_pin_tx: Arc>>>, + usb_event_forwarder_task: Option>, + hybrid_event_forwarder_task: Option>, +} + +impl Drop for InProcessClient { + fn drop(&mut self) { + if let Some(task) = self.usb_event_forwarder_task.take() { + async_std::task::block_on(task.cancel()); + } + + if let Some(task) = self.hybrid_event_forwarder_task.take() { + async_std::task::block_on(task.cancel()); + } + } } impl InProcessClient { @@ -190,30 +202,35 @@ impl CredentialServiceClient for InProcessClient { } } - async fn get_hybrid_credential(&self) -> Result<(), ()> { + async fn get_hybrid_credential(&mut self) -> Result<(), ()> { let response = self .send(ServiceRequest::GetHybridCredential) .await .unwrap(); if let ServiceResponse::GetHybridCredential(mut stream) = response { if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { - while let Some(hybrid_state) = stream.next().await { - if let Some(tx) = tx_weak.upgrade() { - match hybrid_state { - HybridState::Completed | HybridState::Failed => { - tx.send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + let task = async_std::task::spawn(async move { + while let Some(hybrid_state) = stream.next().await { + if let Some(tx) = tx_weak.upgrade() { + match hybrid_state { + HybridState::Completed | HybridState::Failed => { + tx.send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + .await + .unwrap(); + break; + } + _ => tx + .send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) .await - .unwrap(); - break; - } - _ => tx - .send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) - .await - .unwrap(), - }; + .unwrap(), + }; + } } + }); + if let Some(prev_task) = self.hybrid_event_forwarder_task.replace(task) { + prev_task.cancel().await; } - }; + } Ok(()) } else { panic!("Unable to get hybrid credential"); @@ -224,23 +241,31 @@ impl CredentialServiceClient for InProcessClient { let response = self.send(ServiceRequest::GetUsbCredential).await.unwrap(); if let ServiceResponse::GetUsbCredential(mut stream) = response { if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { - while let Some(state) = stream.next().await { - if let Some(tx) = tx_weak.upgrade() { - tx.send(BackgroundEvent::UsbStateChanged((&state).into())) - .await - .unwrap(); - match state { - UsbState::NeedsPin { pin_tx, .. } => { - let _ = self.usb_pin_tx.insert(pin_tx); - } - UsbState::Completed | UsbState::Failed(_) => { + let usb_pin_tx = self.usb_pin_tx.clone(); + let task = async_std::task::spawn(async move { + while let Some(state) = stream.next().await { + if let Some(tx) = tx_weak.upgrade() { + if tx.send(BackgroundEvent::UsbStateChanged((&state).into())).await.is_err() { + tracing::debug!("Closing USB background event forwarder"); break; } - _ => {} - }; + match state { + UsbState::NeedsPin { pin_tx, .. } => { + let mut usb_pin_tx = usb_pin_tx.lock().await; + let _ = usb_pin_tx.insert(pin_tx); + } + UsbState::Completed | UsbState::Failed(_) => { + break; + } + _ => {} + }; + } } + }); + if let Some(prev_task) = self.usb_event_forwarder_task.replace(task){ + prev_task.cancel().await; } - }; + } Ok(()) } else { panic!("Unable to get usb credential"); @@ -249,18 +274,22 @@ impl CredentialServiceClient for InProcessClient { async fn initiate_event_stream( &mut self, - ) -> Result + Send + '_>>, ()> { + ) -> Result + Send + 'static>>, ()> { + + let (tx, mut rx) = mpsc::channel(32); + self.bg_event_tx = Some(tx); Ok(Box::pin(async_stream::stream! { - while let Some(ref mut rx) = self.bg_event_rx { - while let Some(bg_event) = rx.recv().await { - yield bg_event - } + // TODO: we need to add a shutdown event that tells this stream + // to shut down when completed, failed or cancelled + while let Some(bg_event) = rx.recv().await { + yield bg_event } + tracing::debug!("event stream ended"); })) } async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { - if let Some(pin_tx) = self.usb_pin_tx.take() { + if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { pin_tx.send(pin).await.unwrap(); } Ok(()) @@ -276,14 +305,13 @@ impl CredentialServiceClient for Arc { InProcessClient::get_available_public_key_devices(self) } - fn get_hybrid_credential(&self) -> impl Future> { - // ) -> impl Future + Send>>> { - InProcessClient::get_hybrid_credential(self) + async fn get_hybrid_credential(&mut self) -> Result<(), ()> { + let client = Arc::get_mut(self).ok_or(())?; + InProcessClient::get_hybrid_credential(client).await } async fn get_usb_credential( &mut self, - // ) -> impl Future + Send>>> { ) -> Result<(), ()> { let client = Arc::get_mut(self).ok_or(())?; InProcessClient::get_usb_credential(client).await @@ -291,7 +319,7 @@ impl CredentialServiceClient for Arc { async fn initiate_event_stream( &mut self, - ) -> Result + Send + '_>>, ()> { + ) -> Result + Send + 'static>>, ()> { let client = Arc::get_mut(self).ok_or(())?; InProcessClient::initiate_event_stream(client).await } @@ -333,8 +361,9 @@ where let client = InProcessClient { tx: client_tx, bg_event_tx: None, - bg_event_rx: None, - usb_pin_tx: None, + usb_pin_tx: Arc::new(AsyncMutex::new(None)), + usb_event_forwarder_task: None, + hybrid_event_forwarder_task: None, }; (Self { svc, rx }, mgr, client) } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs index be1baf74..4f19b641 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs @@ -1,8 +1,9 @@ pub mod view_model; +use std::sync::Arc; use std::thread; -use async_std::channel::Receiver; +use async_std::{channel::Receiver, sync::Mutex as AsyncMutex}; use tokio::sync::oneshot; use crate::credential_service::CredentialServiceClient; @@ -22,6 +23,7 @@ pub(super) fn start_gui_thread(client: C, request: ViewRequest) { +fn run_gui(client: Arc>, request: ViewRequest) { let ViewRequest { operation, signal: response_tx, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs index 8706847a..5b22c762 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use async_std::prelude::*; use async_std::{ channel::{Receiver, Sender}, - sync::Mutex, + sync::Mutex as AsyncMutex, }; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; @@ -22,7 +22,7 @@ pub(crate) struct ViewModel where C: CredentialServiceClient + Send, { - credential_service: Arc>, + credential_service: Arc>, tx_update: Sender, rx_event: Receiver, bg_event: Receiver, @@ -34,7 +34,7 @@ where selected_device: Option, // providers: Vec, - usb_cred_tx: Option>>>, + usb_cred_tx: Option>>>, hybrid_qr_state: HybridState, hybrid_qr_code_data: Option>, @@ -44,13 +44,13 @@ where impl ViewModel { pub(crate) fn new( operation: Operation, - credential_service: C, + credential_service: Arc>, rx_event: Receiver, tx_update: Sender, ) -> Self { let (bg_update, bg_event) = async_std::channel::unbounded::(); Self { - credential_service: Arc::new(Mutex::new(credential_service)), + credential_service, rx_event, tx_update, bg_event, @@ -117,7 +117,7 @@ impl ViewModel { match device.transport { Transport::Usb => { let mut cred_service = self.credential_service.lock().await; - cred_service.get_usb_credential().await.unwrap(); + (*cred_service).get_usb_credential().await.unwrap(); } Transport::HybridQr => { let mut cred_service = self.credential_service.lock().await; @@ -136,8 +136,11 @@ impl ViewModel { pub(crate) async fn start_event_loop(&mut self) { let view_events = self.rx_event.clone().map(Event::View); - let bg_events = self.bg_event.clone().map(Event::Background); - let mut all_events = view_events.merge(bg_events); + let bg_events = { + let mut cred_service = self.credential_service.lock().await; + cred_service.initiate_event_stream().await.unwrap() + }; + let mut all_events = view_events.merge(bg_events.map(Event::Background)); while let Some(event) = all_events.next().await { match event { Event::View(ViewEvent::Initiated) => { @@ -149,8 +152,7 @@ impl ViewModel { println!("Selected device {id}"); } Event::View(ViewEvent::UsbPinEntered(pin)) => { - let cred_service = self.credential_service.clone(); - let mut cred_service = cred_service.lock().await; + let mut cred_service = self.credential_service.lock().await; if cred_service.enter_client_pin(pin).await.is_err() { error!("Failed to send pin to device"); } From f0b7d0051d9e1f75ae4da29bd3272bb276e2e241 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 22 Jul 2025 10:17:19 -0500 Subject: [PATCH 08/38] Apply clippy lints to dbus.rs --- .../src/dbus.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 5943a91d..905c0f5e 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -211,7 +211,7 @@ impl CredentialManager match *signal_state { SignalState::Idle => {} SignalState::Pending(ref mut pending) => { - for msg in pending.into_iter() { + for msg in pending.iter_mut() { emitter.state_changed(msg).await?; } } @@ -863,7 +863,7 @@ impl TryFrom for ViewUpdate { type Error = zbus::zvariant::Error; fn try_from(value: ClientUpdate) -> std::result::Result { match value { - ClientUpdate::SetTitle(v) => v.try_into().map(|title| Self::SetTitle(title)), + ClientUpdate::SetTitle(v) => v.try_into().map(Self::SetTitle), ClientUpdate::SetDevices(v) => { let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; let devices: std::result::Result, zbus::zvariant::Error> = @@ -886,13 +886,7 @@ impl TryFrom for ViewUpdate { zbus::zvariant::Error, > = dbus_credentials .into_iter() - .map(|creds| { - creds.try_into().map_err(|_| { - zbus::zvariant::Error::Message( - "Could not deserialize credentials".to_string(), - ) - }) - }) + .map(|creds| Ok(creds.into())) .collect(); Ok(Self::SetCredentials(credentials?)) } @@ -918,12 +912,12 @@ impl TryFrom for ViewUpdate { ClientUpdate::HybridNeedsQrCode(v) => v .try_into() - .map(|qr_code_data| Self::HybridNeedsQrCode(qr_code_data)), + .map(Self::HybridNeedsQrCode), ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), ClientUpdate::Completed(_) => Ok(Self::Completed), - ClientUpdate::Failed(v) => v.try_into().map(|error_msg| Self::Failed(error_msg)), + ClientUpdate::Failed(v) => v.try_into().map(Self::Failed), } } } From 16b9df067aa3450d55473b27a8bdf62ad8d44691 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sun, 27 Jul 2025 15:59:54 -0500 Subject: [PATCH 09/38] wip: Work on exposing scoped D-Bus services --- .../src/credential_service/hybrid.rs | 5 +- .../src/credential_service/mod.rs | 6 +- .../src/credential_service/server.rs | 90 +- .../src/credential_service/usb.rs | 5 +- .../src/dbus.rs | 919 +++--------------- .../src/dbus/model.rs | 809 +++++++++++++++ .../src/model.rs | 91 +- .../src/webauthn.rs | 37 +- 8 files changed, 1137 insertions(+), 825 deletions(-) create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.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 8750fc58..a3799799 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 @@ -12,8 +12,7 @@ use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOpe use libwebauthn::transport::{Channel, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; -use crate::dbus::CredentialRequest; -use crate::model::Error; +use crate::model::{CredentialRequest, Error}; use super::AuthenticatorResponse; @@ -250,7 +249,7 @@ pub(super) mod test { proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transport}, }; - use crate::dbus::CredentialRequest; + use crate::model::CredentialRequest; use super::{HybridEvent, HybridHandler, HybridStateInternal}; #[derive(Debug)] 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 5f8d1f66..0c64fd4d 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 @@ -17,8 +17,7 @@ use libwebauthn::{ use crate::{ credential_service::{hybrid::HybridEvent, usb::UsbEvent}, - dbus::{CredentialRequest, CredentialResponse}, - model::{Device, Transport}, + model::{CredentialRequest, CredentialResponse, Device, Transport}, }; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; @@ -214,7 +213,8 @@ mod test { use crate::{ credential_service::usb::InProcessUsbHandler, - dbus::{CreateCredentialRequest, CreatePublicKeyCredentialRequest, CredentialRequest}, + dbus::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}, + model::CredentialRequest, }; use super::{ 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 9b8b1792..623df7fb 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 @@ -7,8 +7,7 @@ use async_std::sync::Mutex as AsyncMutex; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot}; -use crate::dbus::{CredentialRequest, CredentialResponse}; -use crate::model::{BackgroundEvent, Device}; +use crate::model::{BackgroundEvent, CredentialRequest, CredentialResponse, Device}; use super::hybrid::{HybridHandler, HybridState}; use super::usb::{UsbHandler, UsbState}; @@ -73,15 +72,13 @@ pub trait CredentialServiceClient { &self, ) -> impl Future, ()>> + Send; - fn get_hybrid_credential( - &mut self, - ) -> impl Future> + Send; - fn get_usb_credential( - &mut self, - ) -> impl Future> + Send; + fn get_hybrid_credential(&mut self) -> impl Future> + Send; + fn get_usb_credential(&mut self) -> impl Future> + Send; fn initiate_event_stream( &mut self, - ) -> impl Future + Send + 'static>>, ()>> + Send; + ) -> impl Future< + Output = Result + Send + 'static>>, ()>, + > + Send; fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; fn select_credential( &self, @@ -95,6 +92,23 @@ pub trait CredentialManagementClient { cred_request: CredentialRequest, ) -> impl Future> + Send; fn complete_auth(&self) -> impl Future> + Send; + + fn get_available_public_key_devices( + &self, + ) -> impl Future, ()>> + Send; + + fn get_hybrid_credential(&mut self) -> impl Future> + Send; + fn get_usb_credential(&mut self) -> impl Future> + Send; + fn initiate_event_stream( + &mut self, + ) -> impl Future< + Output = Result + Send + 'static>>, ()>, + > + Send; + fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; + fn select_credential( + &self, + credential_id: String, + ) -> impl Future> + Send; } pub struct InProcessManager { @@ -146,6 +160,39 @@ impl CredentialManagementClient for InProcessManager { Err("No credentials in credential service".to_string()) } } + + fn get_available_public_key_devices( + &self, + ) -> impl Future, ()>> + Send { + todo!() + } + + fn get_hybrid_credential(&mut self) -> impl Future> + Send { + todo!() + } + + fn get_usb_credential(&mut self) -> impl Future> + Send { + todo!() + } + + fn initiate_event_stream( + &mut self, + ) -> impl Future< + Output = Result + Send + 'static>>, ()>, + > + Send { + todo!() + } + + fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send { + todo!() + } + + fn select_credential( + &self, + credential_id: String, + ) -> impl Future> + Send { + todo!() + } } pub struct InProcessClient { @@ -214,13 +261,17 @@ impl CredentialServiceClient for InProcessClient { if let Some(tx) = tx_weak.upgrade() { match hybrid_state { HybridState::Completed | HybridState::Failed => { - tx.send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) - .await - .unwrap(); + tx.send(BackgroundEvent::HybridQrStateChanged( + hybrid_state.into(), + )) + .await + .unwrap(); break; } _ => tx - .send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + .send(BackgroundEvent::HybridQrStateChanged( + hybrid_state.into(), + )) .await .unwrap(), }; @@ -245,7 +296,11 @@ impl CredentialServiceClient for InProcessClient { let task = async_std::task::spawn(async move { while let Some(state) = stream.next().await { if let Some(tx) = tx_weak.upgrade() { - if tx.send(BackgroundEvent::UsbStateChanged((&state).into())).await.is_err() { + if tx + .send(BackgroundEvent::UsbStateChanged((&state).into())) + .await + .is_err() + { tracing::debug!("Closing USB background event forwarder"); break; } @@ -262,7 +317,7 @@ impl CredentialServiceClient for InProcessClient { } } }); - if let Some(prev_task) = self.usb_event_forwarder_task.replace(task){ + if let Some(prev_task) = self.usb_event_forwarder_task.replace(task) { prev_task.cancel().await; } } @@ -275,7 +330,6 @@ impl CredentialServiceClient for InProcessClient { async fn initiate_event_stream( &mut self, ) -> Result + Send + 'static>>, ()> { - let (tx, mut rx) = mpsc::channel(32); self.bg_event_tx = Some(tx); Ok(Box::pin(async_stream::stream! { @@ -310,9 +364,7 @@ impl CredentialServiceClient for Arc { InProcessClient::get_hybrid_credential(client).await } - async fn get_usb_credential( - &mut self, - ) -> Result<(), ()> { + async fn get_usb_credential(&mut self) -> Result<(), ()> { let client = Arc::get_mut(self).ok_or(())?; InProcessClient::get_usb_credential(client).await } 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 309faaec..ce5ea665 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 @@ -17,10 +17,7 @@ use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; -use crate::{ - dbus::{CredentialRequest, GetAssertionResponseInternal}, - model::{Credential, Error}, -}; +use crate::model::{Credential, CredentialRequest, Error, GetAssertionResponseInternal}; use super::{AuthenticatorResponse, CredentialResponse}; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 905c0f5e..61b0207d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -1,38 +1,56 @@ -use std::collections::{HashMap, VecDeque}; +//! This module hosts the D-Bus endpoints needed for this service. +//! +//! The D-Bus endpoints are structured to allow sandboxing with small component processes connected with a central broker. +//! # Broker: +//! The broker's main responsibility is to enforce permissions between the various components. +//! To do that, the broker has a bunch of seemingly redundant methods that forwards to the actual +//! implementations. +//! +//! The internal components should sandboxed only to have access to resources needed to fulfill the request. +//! +//! ## Client -> pub service -> broker -> Cred Service: +//! These methods are called by the pub service on behalf of a client requesting credentials. +//! The pub service must pass appropriate context for the broker to determine the client's permissions. +//! - get_cred(options) +//! - create_cred(options) +//! - get_client_capabilities() +//! +//! ## UI -> broker -> Cred service: +//! These methods are called by the trusted UI to interact with the credential service. +//! - initialize_event_stream() +//! - get_hybrid_credential() +//! - get_usb_credential() +//! - get_available_devices() # a device is a discrete authenticator or a group of potential authenticators accessible via a particular transport, or a credential? +//! - send_pin() +//! - select_credential() +//! - cancel_request() +//! +//! ## Cred Service -> broker -> UI: +//! - launch UI +//! - send_state_changed() + +mod model; + +use std::collections::VecDeque; use std::sync::Arc; -use std::time::Duration; - -use base64::Engine; -use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD}; - -use libwebauthn::ops::webauthn::{ - Assertion, CredentialProtectionExtension, GetAssertionHmacOrPrfInput, - GetAssertionLargeBlobExtension, GetAssertionRequest, GetAssertionRequestExtensions, - MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialResponse, - MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement, -}; -use libwebauthn::proto::ctap2::{ - Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, - Ctap2PublicKeyCredentialUserEntity, -}; -use ring::digest; -use serde::{Deserialize, Serialize}; +use futures_lite::StreamExt; use tokio::sync::Mutex as AsyncMutex; use zbus::object_server::SignalEmitter; -use zbus::zvariant::{OwnedValue, Value, LE}; +use zbus::zvariant; use zbus::{ connection::{self, Connection}, fdo, interface, - zvariant::{DeserializeDict, SerializeDict, Type}, Result, }; -use crate::credential_service::CredentialManagementClient; +use crate::credential_service::{CredentialManagementClient, CredentialServiceClient}; use crate::gui::ViewRequest; -use crate::model::{CredentialType, Operation, ViewUpdate}; -use crate::webauthn::{ - self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, -}; +use crate::model::{CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, Operation}; + +use self::model::{BackgroundEvent, CreateCredentialResponse, Device, GetPublicKeyCredentialResponse, CreatePublicKeyCredentialResponse, GetCredentialRequest, GetCredentialResponse}; +// TODO: This is a workaround for testing credential_service. Refactor so that +// these private structs don't need to be exported. +pub use self::model::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}; pub(crate) async fn start_service( service_name: &str, @@ -49,7 +67,6 @@ pub(crate) async fn start_service { app_lock: Arc>>, manager_client: C, - signal_state: Arc>, } +/// These are public methods that can be called by arbitrary clients to begin a credential flow. #[interface(name = "xyz.iinuwa.credentials.CredentialManagerUi1")] impl CredentialManager { async fn create_credential( @@ -202,7 +219,24 @@ impl CredentialManager signal_unknown_credential: false, }) } +} + +struct InternalService { + signal_state: Arc>, +} +/// The following methods are for communication between the [trusted] +/// UI and the credential service, and should not be called by arbitrary +/// clients. +#[interface( + name = "xyz.iinuwa.credentials.CredentialManagerInternal1", + proxy( + gen_blocking = false, + default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", + default_service = "xyz.iinuwa.credentials.CredentialManagerInternal", + ) +)] +impl InternalService { async fn initiate_event_stream( &self, #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, @@ -212,7 +246,7 @@ impl CredentialManager SignalState::Idle => {} SignalState::Pending(ref mut pending) => { for msg in pending.iter_mut() { - emitter.state_changed(msg).await?; + emitter.state_changed(msg.clone()).await?; } } SignalState::Active => {} @@ -220,6 +254,19 @@ impl CredentialManager *signal_state = SignalState::Active; Ok(()) } + + async fn get_available_public_key_devices(&self) -> fdo::Result> { + todo!() + } + + async fn get_hybrid_credential(&self) -> fdo::Result<()> { + todo!() + } + + async fn get_usb_credential(&self) -> fdo::Result<()> { + todo!() + } + async fn select_device(&self, device_id: String) -> fdo::Result<()> { todo!() } @@ -245,7 +292,7 @@ impl CredentialManager pending.push_back(update); } SignalState::Active => { - emitter.state_changed(&update).await?; + emitter.state_changed(update).await?; } }; Ok(()) @@ -254,10 +301,20 @@ impl CredentialManager #[zbus(signal)] async fn state_changed( emitter: &SignalEmitter<'_>, - update: &BackgroundEvent, + update: BackgroundEvent, ) -> zbus::Result<()>; } +struct UiControlService; + +#[interface(name = "xyz.iinuwa.credentials.UiControl1")] +impl UiControlService { + fn launch_ui(&self) {} + fn send_state_changed(&self) { + + } +} + async fn execute_flow( gui_tx: &async_std::channel::Sender, manager_client: &C, @@ -296,780 +353,60 @@ async fn execute_flow( }) } -// D-Bus <-> internal types -#[derive(Clone, Debug)] -pub(crate) enum CredentialRequest { - CreatePublicKeyCredentialRequest(MakeCredentialRequest), - GetPublicKeyCredentialRequest(GetAssertionRequest), -} - -#[derive(Clone, Debug)] -pub(crate) enum CredentialResponse { - CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal), - GetPublicKeyCredentialResponse(GetAssertionResponseInternal), -} - -impl CredentialResponse { - pub(crate) fn from_make_credential( - response: &MakeCredentialResponse, - transports: &[&str], - modality: &str, - ) -> CredentialResponse { - CredentialResponse::CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal::new( - response.clone(), - transports.iter().map(|s| s.to_string()).collect(), - modality.to_string(), - )) - } - - pub(crate) fn from_get_assertion(assertion: &Assertion, modality: &str) -> CredentialResponse { - CredentialResponse::GetPublicKeyCredentialResponse(GetAssertionResponseInternal::new( - assertion.clone(), - modality.to_string(), - )) - } -} - -#[derive(Clone, Debug)] -pub(crate) struct MakeCredentialResponseInternal { - ctap: MakeCredentialResponse, - transport: Vec, - attachment_modality: String, -} - -impl MakeCredentialResponseInternal { - pub(crate) fn new( - response: MakeCredentialResponse, - transport: Vec, - attachment_modality: String, - ) -> Self { - Self { - ctap: response, - transport, - attachment_modality, - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct GetAssertionResponseInternal { - ctap: Assertion, - attachment_modality: String, -} - -impl GetAssertionResponseInternal { - pub(crate) fn new(ctap: Assertion, attachment_modality: String) -> Self { - Self { - ctap, - attachment_modality, - } - } -} - -// D-Bus <-> Client types -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreateCredentialRequest { - pub(crate) origin: Option, - pub(crate) is_same_origin: Option, - #[zvariant(rename = "type")] - pub(crate) r#type: String, - #[zvariant(rename = "publicKey")] - pub(crate) public_key: Option, +struct DbusCredentialClient<'a> { + proxy: InternalServiceProxy<'a> } -impl CreateCredentialRequest { - pub(crate) fn try_into_ctap2_request( +impl CredentialServiceClient for DbusCredentialClient<'_> { + async fn get_available_public_key_devices( &self, - ) -> std::result::Result<(MakeCredentialRequest, String), webauthn::Error> { - if self.public_key.is_none() { - return Err(webauthn::Error::NotSupported); - } - let options = self.public_key.as_ref().unwrap(); - - let request_value = serde_json::from_str::(&options.request_json) - .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; - let json = request_value - .as_object() - .ok_or_else(|| webauthn::Error::Internal("Invalid request JSON".to_string()))?; - let challenge = json - .get("challenge") - .and_then(|c| c.as_str()) - .ok_or_else(|| webauthn::Error::Internal("JSON missing `challenge` field".to_string()))? - .to_owned(); - let rp = json - .get("rp") - .and_then(|val| { - serde_json::from_str::(&val.to_string()).ok() - }) - .ok_or_else(|| webauthn::Error::Internal("JSON missing `rp` field".to_string()))?; - let user = json - .get("user") - .ok_or(webauthn::Error::Internal( - "JSON missing `user` field".to_string(), - )) - .and_then(|val| { - serde_json::from_str::(&val.to_string()) - .map_err(|e| { - let msg = format!("JSON missing `user` field: {e}"); - webauthn::Error::Internal(msg) - }) - })?; - let other_options = - serde_json::from_str::(&request_value.to_string()) - .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; - let (resident_key, user_verification) = - if let Some(authenticator_selection) = other_options.authenticator_selection { - let resident_key = match authenticator_selection.resident_key.as_deref() { - Some("required") => Some(ResidentKeyRequirement::Required), - Some("preferred") => Some(ResidentKeyRequirement::Preferred), - Some("discouraged") => Some(ResidentKeyRequirement::Discouraged), - Some(_) => None, - // legacy webauthn-1 member - None if authenticator_selection.require_resident_key == Some(true) => { - Some(ResidentKeyRequirement::Required) - } - None => None, - }; - - let user_verification = authenticator_selection - .user_verification - .map(|uv| match uv.as_ref() { - "required" => UserVerificationRequirement::Required, - "preferred" => UserVerificationRequirement::Preferred, - "discouraged" => UserVerificationRequirement::Discouraged, - _ => todo!("This should be fixed in the future"), - }) - .unwrap_or(UserVerificationRequirement::Preferred); - - (resident_key, user_verification) - } else { - (None, UserVerificationRequirement::Preferred) - }; - let extensions = if let Some(incoming_extensions) = other_options.extensions { - let extensions = MakeCredentialsRequestExtensions { - cred_props: incoming_extensions.cred_props, - cred_blob: incoming_extensions - .cred_blob - .and_then(|x| URL_SAFE_NO_PAD.decode(x).ok()), - min_pin_length: incoming_extensions.min_pin_length, - cred_protect: match incoming_extensions.credential_protection_policy { - Some(cred_prot_policy) => Some(CredentialProtectionExtension { - policy: cred_prot_policy, - enforce_policy: incoming_extensions - .enforce_credential_protection_policy - .unwrap_or_default(), - }), - None => None, - }, - large_blob: incoming_extensions - .large_blob - .map(|x| x.support.unwrap_or_default()) - .unwrap_or_default(), - hmac_or_prf: if incoming_extensions.prf.is_some() { - // CTAP currently doesn't support PRF queries at credentials.create() - // So we ignore any potential value set in the request and only mark this - // credential to activate HMAC for future PRF queries using credentials.get() - MakeCredentialHmacOrPrfInput::Prf - } else { - // MakeCredentialHmacOrPrfInput::Hmac is not used directly by webauthn - MakeCredentialHmacOrPrfInput::None - }, - }; - Some(extensions) - } else { - None - }; - - let credential_parameters = request_value - .clone() - .get("pubKeyCredParams") - .ok_or_else(|| { - webauthn::Error::Internal( - "Request JSON missing or invalid `pubKeyCredParams` key".to_string(), - ) - }) - .and_then(|val| -> std::result::Result, webauthn::Error> { - serde_json::from_str::>(&val.to_string()) - .map_err(|e| { - webauthn::Error::Internal(format!( - "Request JSON missing or invalid `pubKeyCredParams` key: {e}" - )) - }) - })?; - let algorithms = credential_parameters - .iter() - .filter_map(|p| p.try_into().ok()) - .collect(); - let exclude = other_options.excluded_credentials.map(|v| { - v.iter() - .map(|e| e.try_into()) - .filter_map(|e| e.ok()) - .collect() - }); - let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { - (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), - (Some(origin), None) => (origin.to_string(), true), - // origin should always be set on request either by client or D-Bus service, - // so this shouldn't be called - (None, _) => { - return Err(webauthn::Error::Internal( - "Error reading origin from request".to_string(), - )); - } - }; - let client_data_json = format_client_data_json( - Operation::Create { - cred_type: CredentialType::Passkey, - }, - &challenge, - &origin, - is_cross_origin, - ); - let client_data_hash = digest::digest(&digest::SHA256, client_data_json.as_bytes()) - .as_ref() - .to_owned(); - Ok(( - MakeCredentialRequest { - hash: client_data_hash, - origin, - - relying_party: rp, - user, - resident_key, - user_verification, - algorithms, - exclude, - extensions, - timeout: other_options.timeout.unwrap_or(Duration::from_secs(300)), - }, - client_data_json, - )) + ) -> std::result::Result, ()> { + let dbus_devices = self.proxy.get_available_public_key_devices().await.map_err(|_|())?; + dbus_devices.into_iter().map(|d| d.try_into()).collect() } -} - -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreatePublicKeyCredentialRequest { - pub(crate) request_json: String, -} - -impl CreatePublicKeyCredentialResponse { - fn try_from_ctap2_response( - response: &MakeCredentialResponseInternal, - client_data_json: String, - ) -> std::result::Result { - let auth_data = &response.ctap.authenticator_data; - let attested_credential = auth_data.attested_credential.as_ref().ok_or_else(|| { - fdo::Error::Failed("Invalid credential received from authenticator".to_string()) - })?; - let unsigned_extensions = - serde_json::to_string(&response.ctap.unsigned_extensions_output).unwrap(); - let authenticator_data_blob = auth_data.to_response_bytes().unwrap(); - let attestation_statement = - (&response.ctap.attestation_statement) - .try_into() - .map_err(|_| { - fdo::Error::Failed("Could not serialize attestation statement".to_string()) - })?; - let attestation_object = webauthn::create_attestation_object( - &authenticator_data_blob, - &attestation_statement, - response.ctap.enterprise_attestation.unwrap_or(false), - ) - .map_err(|_| zbus::Error::Failure("Failed to create attestation object".to_string()))?; - // do we need to check that the client_data_hash is the same? - let registration_response_json = webauthn::CreatePublicKeyCredentialResponse::new( - attested_credential.credential_id.clone(), - attestation_object, - client_data_json, - Some(response.transport.clone()), - unsigned_extensions, - response.attachment_modality.clone(), - ) - .to_json(); - let response = CreatePublicKeyCredentialResponse { - registration_response_json, - }; - Ok(response) + async fn get_hybrid_credential( + &mut self, + ) -> std::result::Result<(), ()> { + self.proxy.get_hybrid_credential().await + .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) + .map_err(|_| ()) } -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreateCredentialResponse { - #[zvariant(rename = "type")] - r#type: String, - public_key: Option, -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreatePublicKeyCredentialResponse { - registration_response_json: String, -} -impl From for CreateCredentialResponse { - fn from(response: CreatePublicKeyCredentialResponse) -> Self { - CreateCredentialResponse { - // TODO: Decide on camelCase or kebab-case for cred types - r#type: "public-key".to_string(), - public_key: Some(response), - } + async fn get_usb_credential( + &mut self, + ) -> std::result::Result<(), ()> { + self.proxy.get_hybrid_credential().await + .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) + .map_err(|_| ()) } -} -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetCredentialRequest { - origin: Option, - is_same_origin: Option, - #[zvariant(rename = "type")] - r#type: String, - #[zvariant(rename = "publicKey")] - public_key: Option, -} - -impl GetCredentialRequest { - fn try_into_ctap2_request( - &self, - ) -> std::result::Result<(GetAssertionRequest, String), webauthn::Error> { - if self.public_key.is_none() { - return Err(webauthn::Error::NotSupported); - } - let options = self.public_key.as_ref().unwrap(); - let request: webauthn::GetCredentialOptions = - serde_json::from_str(&options.request_json) - .map_err(|e| webauthn::Error::Internal(format!("Invalid request JSON: {:?}", e)))?; - let mut allow: Vec = request - .allow_credentials - .iter() - .filter_map(|cred| { - if cred.cred_type == "public-key" { - cred.try_into().ok() - } else { - None - } + async fn initiate_event_stream( + &mut self, + ) -> std::result::Result + Send + 'static>>, ()> { + let stream = self.proxy.receive_state_changed().await + .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? + .filter_map(|msg| { + msg.args().and_then(|args| args.update.try_into().map_err(|err: zvariant::Error| err.into())) + .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) + .ok() }) - .collect(); - // TODO: The allow is returning an empty list instead of either None or a list of transports. - // This should be investigated, but this is just a UI hint and isn't necessary to pass to the authenticator. - // Just removing it for now. - for c in allow.iter_mut() { - c.transports = None; - } - let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { - (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), - (Some(origin), None) => (origin.to_string(), true), - // origin should always be set on request either by client or D-Bus service, - // so this shouldn't be called - (None, _) => { - return Err(webauthn::Error::Internal( - "Error reading origin from request".to_string(), - )); - } - }; - let client_data_json = format_client_data_json( - Operation::Get { - cred_types: vec![CredentialType::Passkey], - }, - &request.challenge, - &origin, - is_cross_origin, - ); - let client_data_hash = digest::digest(&digest::SHA256, client_data_json.as_bytes()) - .as_ref() - .to_owned(); - // 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 request - .user_verification - .unwrap_or_else(|| String::from("preferred")) - .as_ref() - { - "required" => UserVerificationRequirement::Required, - "preferred" => UserVerificationRequirement::Preferred, - "discouraged" => UserVerificationRequirement::Discouraged, - _ => { - return Err(webauthn::Error::Internal( - "Invalid user verification requirement specified".to_string(), - )) - } - }; - let relying_party_id = request.rp_id.unwrap_or_else(|| { - let (_, effective_domain) = origin.rsplit_once('/').unwrap(); - effective_domain.to_string() - }); - - let extensions = if let Some(incoming_extensions) = request.extensions { - let extensions = GetAssertionRequestExtensions { - cred_blob: incoming_extensions.get_cred_blob, - hmac_or_prf: incoming_extensions - .prf - .and_then(|x| { - x.eval.map(|eval| { - let eval = Some(eval.decode()); - let mut eval_by_credential = HashMap::new(); - if let Some(incoming_eval) = x.eval_by_credential { - for (key, val) in incoming_eval.iter() { - eval_by_credential.insert(key.clone(), val.decode()); - } - } - GetAssertionHmacOrPrfInput::Prf { - eval, - eval_by_credential, - } - }) - }) - .unwrap_or_default(), - large_blob: incoming_extensions - .large_blob - // TODO: Implement GetAssertionLargeBlobExtension::Write, once libwebauthn supports it - .filter(|x| x.read == Some(true)) - .map(|_| GetAssertionLargeBlobExtension::Read) - .unwrap_or(GetAssertionLargeBlobExtension::None), - }; - Some(extensions) - } else { - None - }; - - Ok(( - GetAssertionRequest { - hash: client_data_hash, - relying_party_id, - user_verification, - allow, - extensions, - timeout: request.timeout.unwrap_or(Duration::from_secs(300)), - }, - client_data_json, - )) - } -} - -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetPublicKeyCredentialRequest { - pub(crate) request_json: String, -} - -impl GetPublicKeyCredentialResponse { - fn try_from_ctap2_response( - response: &GetAssertionResponseInternal, - client_data_json: String, - ) -> std::result::Result { - let authenticator_data_blob = response - .ctap - .authenticator_data - .to_response_bytes() - .unwrap(); - - // We can't just do this here, because we need encode all byte arrays for the JS-communication: - // let unsigned_extensions = response - // .ctap - // .unsigned_extensions_output - // .as_ref() - // .map(|extensions| serde_json::to_string(&extensions).unwrap()); - let unsigned_extensions = response - .ctap - .unsigned_extensions_output - .as_ref() - .map(GetPublicKeyCredentialUnsignedExtensionsResponse::from); - - let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( - client_data_json, - response - .ctap - .credential_id - .as_ref() - .map(|c| c.id.clone().into_vec()), - authenticator_data_blob, - response.ctap.signature.clone(), - response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), - response.attachment_modality.clone(), - unsigned_extensions, - ) - .to_json(); - - let response = GetPublicKeyCredentialResponse { - authentication_response_json, - }; - Ok(response) - } -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetCredentialResponse { - #[zvariant(rename = "type")] - r#type: String, - public_key: Option, -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetPublicKeyCredentialResponse { - authentication_response_json: String, -} - -impl From for GetCredentialResponse { - fn from(response: GetPublicKeyCredentialResponse) -> Self { - GetCredentialResponse { - // TODO: Decide on camelCase or kebab-case for cred types - r#type: "public-key".to_string(), - public_key: Some(response), - } - } -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict", rename_all = "camelCase")] -pub struct GetClientCapabilitiesResponse { - conditional_create: bool, - conditional_get: bool, - hybrid_transport: bool, - passkey_platform_authenticator: bool, - user_verifying_platform_authenticator: bool, - related_origins: bool, - signal_all_accepted_credentials: bool, - signal_current_user_details: bool, - signal_unknown_credential: bool, -} - -/// Updates to send to the client -#[derive(Serialize, Deserialize, Type)] -pub enum ClientUpdate { - SetTitle(OwnedValue), - SetDevices(OwnedValue), - SetCredentials(OwnedValue), - - WaitingForDevice(OwnedValue), - SelectingDevice(OwnedValue), - - UsbNeedsPin(OwnedValue), - UsbNeedsUserVerification(OwnedValue), - UsbNeedsUserPresence(OwnedValue), - - HybridNeedsQrCode(OwnedValue), - HybridConnecting(OwnedValue), - HybridConnected(OwnedValue), - - Completed(OwnedValue), - Failed(OwnedValue), -} - -impl TryFrom for ViewUpdate { - type Error = zbus::zvariant::Error; - fn try_from(value: ClientUpdate) -> std::result::Result { - match value { - ClientUpdate::SetTitle(v) => v.try_into().map(Self::SetTitle), - ClientUpdate::SetDevices(v) => { - let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; - let devices: std::result::Result, zbus::zvariant::Error> = - dbus_devices - .into_iter() - .map(|d| { - d.try_into().map_err(|_| { - zbus::zvariant::Error::Message( - "Could not deserialize devices".to_string(), - ) - }) - }) - .collect(); - Ok(Self::SetDevices(devices?)) - } - ClientUpdate::SetCredentials(v) => { - let dbus_credentials: Vec = Value::<'_>::from(v).try_into()?; - let credentials: std::result::Result< - Vec, - zbus::zvariant::Error, - > = dbus_credentials - .into_iter() - .map(|creds| Ok(creds.into())) - .collect(); - Ok(Self::SetCredentials(credentials?)) - } - - ClientUpdate::WaitingForDevice(v) => { - let dbus_device: Device = Value::<'_>::from(v).try_into()?; - let device: crate::model::Device = dbus_device.try_into().map_err(|_| { - zbus::zvariant::Error::Message("Could not deserialize device".to_string()) - })?; - Ok(Self::WaitingForDevice(device)) - } - ClientUpdate::SelectingDevice(_) => Ok(Self::SelectingDevice), - - ClientUpdate::UsbNeedsPin(v) => v.try_into().map(|x: i32| { - let attempts_left = if x == -1 { None } else { Some(x as u32) }; - Self::UsbNeedsPin { attempts_left } - }), - ClientUpdate::UsbNeedsUserVerification(v) => v.try_into().map(|x: i32| { - let attempts_left = if x == -1 { None } else { Some(x as u32) }; - Self::UsbNeedsUserVerification { attempts_left } - }), - ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), - - ClientUpdate::HybridNeedsQrCode(v) => v - .try_into() - .map(Self::HybridNeedsQrCode), - ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), - ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), - - ClientUpdate::Completed(_) => Ok(Self::Completed), - ClientUpdate::Failed(v) => v.try_into().map(Self::Failed), - } - } -} - -#[derive(SerializeDict, DeserializeDict, Type)] -struct Credential { - id: String, - name: String, - username: String, -} - -impl From for crate::model::Credential { - fn from(value: Credential) -> Self { - Self { - id: value.id, - name: value.name, - username: if value.username.is_empty() { - None - } else { - Some(value.username) - }, - } + .boxed(); + self.proxy.initiate_event_stream().await + .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) + .and_then(|_| Ok(stream)) } -} - -impl TryFrom> for Credential { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let credential: Credential = encoded.deserialize()?.0; - Ok(credential) - } -} - -#[derive(SerializeDict, DeserializeDict, Type)] -struct Device { - id: String, - transport: String, -} -impl TryFrom> for Device { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let device: Device = encoded.deserialize()?.0; - Ok(device) + async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { + self.proxy.enter_client_pin(pin).await + .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) } -} - -impl TryFrom for crate::model::Device { - type Error = (); - fn try_from(value: Device) -> std::result::Result { - let transport = value.transport.try_into().map_err(|_| ())?; - Ok(Self { - id: value.id, - transport, - }) - } -} -#[derive(Clone, Debug, Serialize, Deserialize, Type)] -enum HybridState { - /// Default state, not listening for hybrid transport. - Idle(OwnedValue), - - /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. - Started(OwnedValue), - - /// BLE advert received, connecting to caBLE tunnel with shared secret. - Connecting(OwnedValue), - - /// Connected to device via caBLE tunnel. - Connected(OwnedValue), - - /// Credential received over tunnel. - Completed(OwnedValue), - - // This isn't actually sent from the server. - UserCancelled(OwnedValue), - - /// Failed to receive a credential - Failed(OwnedValue), -} - -impl TryFrom for crate::model::HybridState { - type Error = zbus::zvariant::Error; - fn try_from(value: HybridState) -> std::result::Result { - match value { - HybridState::Idle(_) => Ok(Self::Idle), - HybridState::Started(value) => value.try_into().map(Self::Started), - HybridState::Connecting(_) => Ok(Self::Connecting), - HybridState::Connected(_) => Ok(Self::Connected), - HybridState::Completed(_) => Ok(Self::Completed), - HybridState::UserCancelled(_) => Ok(Self::UserCancelled), - HybridState::Failed(_) => Ok(Self::Failed), - } - } -} - -/// Used to de-/serialize state D-Bus and model::UsbState. -#[derive(Serialize, Deserialize, Type)] -enum UsbState { - Idle(OwnedValue), - Waiting(OwnedValue), - SelectingDevice(OwnedValue), - Connected(OwnedValue), - NeedsPin(OwnedValue), /* { - attempts_left: Option, - }, - */ - NeedsUserVerification(OwnedValue), /* { - attempts_left: Option, - },*/ - - NeedsUserPresence(OwnedValue), - //UserCancelled, - SelectCredential(OwnedValue), /* { - creds: Vec, - },*/ - Completed(OwnedValue), - // Failed(crate::credential_service::Error), - Failed(OwnedValue), -} - -#[derive(Serialize, Deserialize, Type)] -enum BackgroundEvent { - UsbStateChanged(OwnedValue), - HybridStateChanged(OwnedValue), -} - -impl TryFrom> for UsbState { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let obj: Self = encoded.deserialize()?.0; - Ok(obj) + async fn select_credential( + &self, + credential_id: String, + ) -> std::result::Result<(), ()> { + self.proxy.select_credential(credential_id).await + .map_err(|err| tracing::error!("Failed to select credential: {err}")) } } - -fn format_client_data_json( - op: Operation, - challenge: &str, - origin: &str, - is_cross_origin: bool, -) -> String { - let op_str = match op { - Operation::Create { .. } => "webauthn.create", - Operation::Get { .. } => "webauthn.get", - }; - let cross_origin_str = if is_cross_origin { "true" } else { "false" }; - format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge}\",\"origin\":\"{origin}\",\"crossOrigin\":{cross_origin_str}}}") -} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs new file mode 100644 index 00000000..b1843a25 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs @@ -0,0 +1,809 @@ +//! This module contains types used for serializing data to and from D-Bus method calls. +//! +//! Types shared between components within this service belong in crate::model. + +use std::{collections::HashMap, time::Duration}; + +use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use serde::{Deserialize, Serialize}; +use zbus::{ + fdo, + zvariant::{ + self, DeserializeDict, OwnedValue, SerializeDict, Type, Value, LE + }, +}; + +use crate::model::{ + CredentialType, GetAssertionResponseInternal, + MakeCredentialResponseInternal, Operation, ViewUpdate +}; +use crate::webauthn::{ + self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, + CredentialProtectionExtension, GetAssertionHmacOrPrfInput, + GetAssertionLargeBlobExtension, GetAssertionRequest, GetAssertionRequestExtensions, + MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement, + + Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, + Ctap2PublicKeyCredentialUserEntity, +}; + +// D-Bus <-> Client types +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub(super) enum BackgroundEvent { + UsbStateChanged(OwnedValue), + HybridStateChanged(OwnedValue), +} + +impl TryFrom for crate::model::BackgroundEvent { + type Error = zvariant::Error; + + fn try_from(value: BackgroundEvent) -> Result { + let ret = match value { + BackgroundEvent::HybridStateChanged(hybrid_state_val) => { + HybridState::try_from(Value::<'_>::from(hybrid_state_val)) + .and_then(crate::model::HybridState::try_from) + .map(crate::model::BackgroundEvent::HybridQrStateChanged) + } + BackgroundEvent::UsbStateChanged(usb_state_val) => { + UsbState::try_from(Value::<'_>::from(usb_state_val)) + .and_then(crate::model::UsbState::try_from) + .map(crate::model::BackgroundEvent::UsbStateChanged) + } + }?; + Ok(ret) + } +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreateCredentialRequest { + pub(crate) origin: Option, + pub(crate) is_same_origin: Option, + #[zvariant(rename = "type")] + pub(crate) r#type: String, + #[zvariant(rename = "publicKey")] + pub(crate) public_key: Option, +} + +impl CreateCredentialRequest { + pub(crate) fn try_into_ctap2_request( + &self, + ) -> std::result::Result<(MakeCredentialRequest, String), webauthn::Error> { + if self.public_key.is_none() { + return Err(webauthn::Error::NotSupported); + } + let options = self.public_key.as_ref().unwrap(); + + let request_value = serde_json::from_str::(&options.request_json) + .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let json = request_value + .as_object() + .ok_or_else(|| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let challenge = json + .get("challenge") + .and_then(|c| c.as_str()) + .ok_or_else(|| webauthn::Error::Internal("JSON missing `challenge` field".to_string()))? + .to_owned(); + let rp = json + .get("rp") + .and_then(|val| { + serde_json::from_str::(&val.to_string()).ok() + }) + .ok_or_else(|| webauthn::Error::Internal("JSON missing `rp` field".to_string()))?; + let user = json + .get("user") + .ok_or(webauthn::Error::Internal( + "JSON missing `user` field".to_string(), + )) + .and_then(|val| { + serde_json::from_str::(&val.to_string()) + .map_err(|e| { + let msg = format!("JSON missing `user` field: {e}"); + webauthn::Error::Internal(msg) + }) + })?; + let other_options = + serde_json::from_str::(&request_value.to_string()) + .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let (resident_key, user_verification) = + if let Some(authenticator_selection) = other_options.authenticator_selection { + let resident_key = match authenticator_selection.resident_key.as_deref() { + Some("required") => Some(ResidentKeyRequirement::Required), + Some("preferred") => Some(ResidentKeyRequirement::Preferred), + Some("discouraged") => Some(ResidentKeyRequirement::Discouraged), + Some(_) => None, + // legacy webauthn-1 member + None if authenticator_selection.require_resident_key == Some(true) => { + Some(ResidentKeyRequirement::Required) + } + None => None, + }; + + let user_verification = authenticator_selection + .user_verification + .map(|uv| match uv.as_ref() { + "required" => UserVerificationRequirement::Required, + "preferred" => UserVerificationRequirement::Preferred, + "discouraged" => UserVerificationRequirement::Discouraged, + _ => todo!("This should be fixed in the future"), + }) + .unwrap_or(UserVerificationRequirement::Preferred); + + (resident_key, user_verification) + } else { + (None, UserVerificationRequirement::Preferred) + }; + let extensions = if let Some(incoming_extensions) = other_options.extensions { + let extensions = MakeCredentialsRequestExtensions { + cred_props: incoming_extensions.cred_props, + cred_blob: incoming_extensions + .cred_blob + .and_then(|x| URL_SAFE_NO_PAD.decode(x).ok()), + min_pin_length: incoming_extensions.min_pin_length, + cred_protect: match incoming_extensions.credential_protection_policy { + Some(cred_prot_policy) => Some(CredentialProtectionExtension { + policy: cred_prot_policy, + enforce_policy: incoming_extensions + .enforce_credential_protection_policy + .unwrap_or_default(), + }), + None => None, + }, + large_blob: incoming_extensions + .large_blob + .map(|x| x.support.unwrap_or_default()) + .unwrap_or_default(), + hmac_or_prf: if incoming_extensions.prf.is_some() { + // CTAP currently doesn't support PRF queries at credentials.create() + // So we ignore any potential value set in the request and only mark this + // credential to activate HMAC for future PRF queries using credentials.get() + MakeCredentialHmacOrPrfInput::Prf + } else { + // MakeCredentialHmacOrPrfInput::Hmac is not used directly by webauthn + MakeCredentialHmacOrPrfInput::None + }, + }; + Some(extensions) + } else { + None + }; + + let credential_parameters = request_value + .clone() + .get("pubKeyCredParams") + .ok_or_else(|| { + webauthn::Error::Internal( + "Request JSON missing or invalid `pubKeyCredParams` key".to_string(), + ) + }) + .and_then(|val| -> std::result::Result, webauthn::Error> { + serde_json::from_str::>(&val.to_string()) + .map_err(|e| { + webauthn::Error::Internal(format!( + "Request JSON missing or invalid `pubKeyCredParams` key: {e}" + )) + }) + })?; + let algorithms = credential_parameters + .iter() + .filter_map(|p| p.try_into().ok()) + .collect(); + let exclude = other_options.excluded_credentials.map(|v| { + v.iter() + .map(|e| e.try_into()) + .filter_map(|e| e.ok()) + .collect() + }); + let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { + (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), + (Some(origin), None) => (origin.to_string(), true), + // origin should always be set on request either by client or D-Bus service, + // so this shouldn't be called + (None, _) => { + return Err(webauthn::Error::Internal( + "Error reading origin from request".to_string(), + )); + } + }; + let client_data_json = webauthn::format_client_data_json( + Operation::Create { + cred_type: CredentialType::Passkey, + }, + &challenge, + &origin, + is_cross_origin, + ); + let client_data_hash = webauthn::create_client_data_hash(&client_data_json); + Ok(( + MakeCredentialRequest { + hash: client_data_hash, + origin, + + relying_party: rp, + user, + resident_key, + user_verification, + algorithms, + exclude, + extensions, + timeout: other_options.timeout.unwrap_or(Duration::from_secs(300)), + }, + client_data_json, + )) + } +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreateCredentialResponse { + #[zvariant(rename = "type")] + r#type: String, + public_key: Option, +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreatePublicKeyCredentialRequest { + pub(crate) request_json: String, +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreatePublicKeyCredentialResponse { + registration_response_json: String, +} + +impl CreatePublicKeyCredentialResponse { + pub(super) fn try_from_ctap2_response( + response: &MakeCredentialResponseInternal, + client_data_json: String, + ) -> std::result::Result { + let auth_data = &response.ctap.authenticator_data; + let attested_credential = auth_data.attested_credential.as_ref().ok_or_else(|| { + fdo::Error::Failed("Invalid credential received from authenticator".to_string()) + })?; + + let unsigned_extensions = + serde_json::to_string(&response.ctap.unsigned_extensions_output).unwrap(); + let authenticator_data_blob = auth_data.to_response_bytes().unwrap(); + let attestation_statement = + (&response.ctap.attestation_statement) + .try_into() + .map_err(|_| { + fdo::Error::Failed("Could not serialize attestation statement".to_string()) + })?; + let attestation_object = webauthn::create_attestation_object( + &authenticator_data_blob, + &attestation_statement, + response.ctap.enterprise_attestation.unwrap_or(false), + ) + .map_err(|_| zbus::Error::Failure("Failed to create attestation object".to_string()))?; + // do we need to check that the client_data_hash is the same? + let registration_response_json = webauthn::CreatePublicKeyCredentialResponse::new( + attested_credential.credential_id.clone(), + attestation_object, + client_data_json, + Some(response.transport.clone()), + unsigned_extensions, + response.attachment_modality.clone(), + ) + .to_json(); + let response = CreatePublicKeyCredentialResponse { + registration_response_json, + }; + Ok(response) + } +} + +impl From for CreateCredentialResponse { + fn from(response: CreatePublicKeyCredentialResponse) -> Self { + CreateCredentialResponse { + // TODO: Decide on camelCase or kebab-case for cred types + r#type: "public-key".to_string(), + public_key: Some(response), + } + } +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetCredentialRequest { + pub(super) origin: Option, + pub(super) is_same_origin: Option, + #[zvariant(rename = "type")] + pub(super) r#type: String, + #[zvariant(rename = "publicKey")] + pub(super) public_key: Option, +} + +impl GetCredentialRequest { + pub(super) fn try_into_ctap2_request( + &self, + ) -> std::result::Result<(GetAssertionRequest, String), webauthn::Error> { + if self.public_key.is_none() { + return Err(webauthn::Error::NotSupported); + } + let options = self.public_key.as_ref().unwrap(); + let request: webauthn::GetCredentialOptions = + serde_json::from_str(&options.request_json) + .map_err(|e| webauthn::Error::Internal(format!("Invalid request JSON: {:?}", e)))?; + let mut allow: Vec = request + .allow_credentials + .iter() + .filter_map(|cred| { + if cred.cred_type == "public-key" { + cred.try_into().ok() + } else { + None + } + }) + .collect(); + // TODO: The allow is returning an empty list instead of either None or a list of transports. + // This should be investigated, but this is just a UI hint and isn't necessary to pass to the authenticator. + // Just removing it for now. + for c in allow.iter_mut() { + c.transports = None; + } + let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { + (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), + (Some(origin), None) => (origin.to_string(), true), + // origin should always be set on request either by client or D-Bus service, + // so this shouldn't be called + (None, _) => { + return Err(webauthn::Error::Internal( + "Error reading origin from request".to_string(), + )); + } + }; + let client_data_json = webauthn::format_client_data_json( + Operation::Get { + cred_types: vec![CredentialType::Passkey], + }, + &request.challenge, + &origin, + is_cross_origin, + ); + 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 request + .user_verification + .unwrap_or_else(|| String::from("preferred")) + .as_ref() + { + "required" => UserVerificationRequirement::Required, + "preferred" => UserVerificationRequirement::Preferred, + "discouraged" => UserVerificationRequirement::Discouraged, + _ => { + return Err(webauthn::Error::Internal( + "Invalid user verification requirement specified".to_string(), + )) + } + }; + let relying_party_id = request.rp_id.unwrap_or_else(|| { + let (_, effective_domain) = origin.rsplit_once('/').unwrap(); + effective_domain.to_string() + }); + + let extensions = if let Some(incoming_extensions) = request.extensions { + let extensions = GetAssertionRequestExtensions { + cred_blob: incoming_extensions.get_cred_blob, + hmac_or_prf: incoming_extensions + .prf + .and_then(|x| { + x.eval.map(|eval| { + let eval = Some(eval.decode()); + let mut eval_by_credential = HashMap::new(); + if let Some(incoming_eval) = x.eval_by_credential { + for (key, val) in incoming_eval.iter() { + eval_by_credential.insert(key.clone(), val.decode()); + } + } + GetAssertionHmacOrPrfInput::Prf { + eval, + eval_by_credential, + } + }) + }) + .unwrap_or_default(), + large_blob: incoming_extensions + .large_blob + // TODO: Implement GetAssertionLargeBlobExtension::Write, once libwebauthn supports it + .filter(|x| x.read == Some(true)) + .map(|_| GetAssertionLargeBlobExtension::Read) + .unwrap_or(GetAssertionLargeBlobExtension::None), + }; + Some(extensions) + } else { + None + }; + + Ok(( + GetAssertionRequest { + hash: client_data_hash, + relying_party_id, + user_verification, + allow, + extensions, + timeout: request.timeout.unwrap_or(Duration::from_secs(300)), + }, + client_data_json, + )) + } +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetPublicKeyCredentialRequest { + pub(crate) request_json: String, +} + + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetCredentialResponse { + #[zvariant(rename = "type")] + r#type: String, + public_key: Option, +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetPublicKeyCredentialResponse { + authentication_response_json: String, +} + +impl GetPublicKeyCredentialResponse { + pub(super) fn try_from_ctap2_response( + response: &GetAssertionResponseInternal, + client_data_json: String, + ) -> std::result::Result { + let authenticator_data_blob = response + .ctap + .authenticator_data + .to_response_bytes() + .unwrap(); + + // We can't just do this here, because we need encode all byte arrays for the JS-communication: + // let unsigned_extensions = response + // .ctap + // .unsigned_extensions_output + // .as_ref() + // .map(|extensions| serde_json::to_string(&extensions).unwrap()); + let unsigned_extensions = response + .ctap + .unsigned_extensions_output + .as_ref() + .map(GetPublicKeyCredentialUnsignedExtensionsResponse::from); + + let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( + client_data_json, + response + .ctap + .credential_id + .as_ref() + .map(|c| c.id.clone().into_vec()), + authenticator_data_blob, + response.ctap.signature.clone(), + response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), + response.attachment_modality.clone(), + unsigned_extensions, + ) + .to_json(); + + let response = GetPublicKeyCredentialResponse { + authentication_response_json, + }; + Ok(response) + } +} + +impl From for GetCredentialResponse { + fn from(response: GetPublicKeyCredentialResponse) -> Self { + GetCredentialResponse { + // TODO: Decide on camelCase or kebab-case for cred types + r#type: "public-key".to_string(), + public_key: Some(response), + } + } +} + +/// Updates to send to the client +#[derive(Serialize, Deserialize, Type)] +pub enum ClientUpdate { + SetTitle(OwnedValue), + SetDevices(OwnedValue), + SetCredentials(OwnedValue), + + WaitingForDevice(OwnedValue), + SelectingDevice(OwnedValue), + + UsbNeedsPin(OwnedValue), + UsbNeedsUserVerification(OwnedValue), + UsbNeedsUserPresence(OwnedValue), + + HybridNeedsQrCode(OwnedValue), + HybridConnecting(OwnedValue), + HybridConnected(OwnedValue), + + Completed(OwnedValue), + Failed(OwnedValue), +} + +impl TryFrom for ViewUpdate { + type Error = zbus::zvariant::Error; + fn try_from(value: ClientUpdate) -> std::result::Result { + match value { + ClientUpdate::SetTitle(v) => v.try_into().map(Self::SetTitle), + ClientUpdate::SetDevices(v) => { + let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; + let devices: std::result::Result, zbus::zvariant::Error> = + dbus_devices + .into_iter() + .map(|d| { + d.try_into().map_err(|_| { + zbus::zvariant::Error::Message( + "Could not deserialize devices".to_string(), + ) + }) + }) + .collect(); + Ok(Self::SetDevices(devices?)) + } + ClientUpdate::SetCredentials(v) => { + let dbus_credentials: Vec = Value::<'_>::from(v).try_into()?; + let credentials: std::result::Result< + Vec, + zbus::zvariant::Error, + > = dbus_credentials + .into_iter() + .map(|creds| Ok(creds.into())) + .collect(); + Ok(Self::SetCredentials(credentials?)) + } + + ClientUpdate::WaitingForDevice(v) => { + let dbus_device: Device = Value::<'_>::from(v).try_into()?; + let device: crate::model::Device = dbus_device.try_into().map_err(|_| { + zbus::zvariant::Error::Message("Could not deserialize device".to_string()) + })?; + Ok(Self::WaitingForDevice(device)) + } + ClientUpdate::SelectingDevice(_) => Ok(Self::SelectingDevice), + + ClientUpdate::UsbNeedsPin(v) => v.try_into().map(|x: i32| { + let attempts_left = if x == -1 { None } else { Some(x as u32) }; + Self::UsbNeedsPin { attempts_left } + }), + ClientUpdate::UsbNeedsUserVerification(v) => v.try_into().map(|x: i32| { + let attempts_left = if x == -1 { None } else { Some(x as u32) }; + Self::UsbNeedsUserVerification { attempts_left } + }), + ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), + + ClientUpdate::HybridNeedsQrCode(v) => v + .try_into() + .map(Self::HybridNeedsQrCode), + ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), + ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), + + ClientUpdate::Completed(_) => Ok(Self::Completed), + ClientUpdate::Failed(v) => v.try_into().map(Self::Failed), + } + } +} + +#[derive(SerializeDict, DeserializeDict, Type)] +pub(super) struct Credential { + id: String, + name: String, + username: String, +} + +impl From for crate::model::Credential { + fn from(value: Credential) -> Self { + Self { + id: value.id, + name: value.name, + username: if value.username.is_empty() { + None + } else { + Some(value.username) + }, + } + } +} + +impl TryFrom> for Credential { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let credential: Credential = encoded.deserialize()?.0; + Ok(credential) + } +} + +#[derive(SerializeDict, DeserializeDict, Type)] +pub(super) struct Device { + id: String, + transport: String, +} + +impl TryFrom> for Device { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let device: Device = encoded.deserialize()?.0; + Ok(device) + } +} + +impl TryFrom for crate::model::Device { + type Error = (); + fn try_from(value: Device) -> std::result::Result { + let transport = value.transport.try_into().map_err(|_| ())?; + Ok(Self { + id: value.id, + transport, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub(super) enum HybridState { + /// Default state, not listening for hybrid transport. + Idle(OwnedValue), + + /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. + Started(OwnedValue), + + /// BLE advert received, connecting to caBLE tunnel with shared secret. + Connecting(OwnedValue), + + /// Connected to device via caBLE tunnel. + Connected(OwnedValue), + + /// Credential received over tunnel. + Completed(OwnedValue), + + // This isn't actually sent from the server. + UserCancelled(OwnedValue), + + /// Failed to receive a credential + Failed(OwnedValue), +} + +impl TryFrom for crate::model::HybridState { + type Error = zbus::zvariant::Error; + fn try_from(value: HybridState) -> std::result::Result { + match value { + HybridState::Idle(_) => Ok(Self::Idle), + HybridState::Started(value) => value.try_into().map(Self::Started), + HybridState::Connecting(_) => Ok(Self::Connecting), + HybridState::Connected(_) => Ok(Self::Connected), + HybridState::Completed(_) => Ok(Self::Completed), + HybridState::UserCancelled(_) => Ok(Self::UserCancelled), + HybridState::Failed(_) => Ok(Self::Failed), + } + } +} + +impl TryFrom> for HybridState { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} + +#[derive(Serialize, Deserialize, Type)] +pub(super) enum ServiceError { + /// Some unknown error with the authenticator occurred. + AuthenticatorError, + + /// No matching credentials were found on the device. + NoCredentials, + + /// Too many incorrect PIN attempts, and authenticator must be removed and + /// reinserted to continue any more PIN attempts. + /// + /// Note that this is different than exhausting the PIN count that fully + /// locks out the device. + PinAttemptsExhausted, + + // TODO: We may want to hide the details on this variant from the public API. + /// Something went wrong with the credential service itself, not the authenticator. + Internal, +} + +impl TryFrom> for ServiceError { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} + +impl From for crate::model::Error { + fn from(value: ServiceError) -> Self { + match value { + ServiceError::AuthenticatorError => Self::AuthenticatorError, + ServiceError::NoCredentials => Self::NoCredentials, + ServiceError::PinAttemptsExhausted => Self::PinAttemptsExhausted, + // TODO: this is bogus, we should refactor to remove the tuple field + // and let the client decide how to render the error. + ServiceError::Internal => Self::Internal("Something went wrong. Please try again later.".to_string()), + } + } + +} + +/// Used to de-/serialize state D-Bus and model::UsbState. +#[derive(Serialize, Deserialize, Type)] +pub(super) enum UsbState { + Idle(OwnedValue), + Waiting(OwnedValue), + SelectingDevice(OwnedValue), + Connected(OwnedValue), + NeedsPin(OwnedValue), /* { + attempts_left: Option, + }, + */ + NeedsUserVerification(OwnedValue), /* { + attempts_left: Option, + },*/ + + NeedsUserPresence(OwnedValue), + //UserCancelled, + SelectCredential(OwnedValue), /* { + creds: Vec, + },*/ + Completed(OwnedValue), + // Failed(crate::credential_service::Error), + Failed(OwnedValue), +} + +impl TryFrom for crate::model::UsbState { + type Error = zbus::zvariant::Error; + fn try_from(value: UsbState) -> std::result::Result { + let ret = match value { + UsbState::Idle(_) => Ok(Self::Idle), + UsbState::Waiting(_) => Ok(Self::Waiting), + UsbState::SelectingDevice(_) => Ok(Self::SelectingDevice), + UsbState::Connected(_) => Ok(Self::Connected), + UsbState::NeedsPin(value) => value.try_into().map(|attempts_left| { + let attempts_left = if attempts_left < 0 { None } else { Some(attempts_left) }; + Self::NeedsPin { attempts_left } + }), + UsbState::NeedsUserVerification(value) => value.try_into().map(|attempts_left| { + let attempts_left = if attempts_left < 0 { None } else { Some(attempts_left) }; + Self::NeedsUserVerification { attempts_left } + }), + UsbState::NeedsUserPresence(_) => Ok(Self::NeedsUserPresence), + UsbState::SelectCredential(value) => { + value.try_into() + .map(|creds: Vec| creds.into_iter().map(crate::model::Credential::from).collect()) + .map(|creds| Self::SelectCredential { creds }) + }, + UsbState::Completed(_) => Ok(Self::Completed), + UsbState::Failed(value) => ServiceError::try_from(Value::<'_>::from(value)).map(|err| Self::Failed(err.into())), + }?; + Ok(ret) + } +} + +impl TryFrom> for UsbState { + type Error = zbus::zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zbus::zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} + diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs index 1d57a59d..7eed6def 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs @@ -1,4 +1,7 @@ use serde::{Deserialize, Serialize}; +use zbus:: zvariant::{SerializeDict, Type}; + +use crate::webauthn::{Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse}; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Credential { @@ -7,6 +10,89 @@ pub struct Credential { pub(crate) username: Option, } +#[derive(Clone, Debug)] +pub(crate) enum CredentialRequest { + CreatePublicKeyCredentialRequest(MakeCredentialRequest), + GetPublicKeyCredentialRequest(GetAssertionRequest), +} + +#[derive(Clone, Debug)] +pub(crate) enum CredentialResponse { + CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal), + GetPublicKeyCredentialResponse(GetAssertionResponseInternal), +} + +impl CredentialResponse { + pub(crate) fn from_make_credential( + response: &MakeCredentialResponse, + transports: &[&str], + modality: &str, + ) -> CredentialResponse { + CredentialResponse::CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal::new( + response.clone(), + transports.iter().map(|s| s.to_string()).collect(), + modality.to_string(), + )) + } + + pub(crate) fn from_get_assertion(assertion: &Assertion, modality: &str) -> CredentialResponse { + CredentialResponse::GetPublicKeyCredentialResponse(GetAssertionResponseInternal::new( + assertion.clone(), + modality.to_string(), + )) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct MakeCredentialResponseInternal { + pub(crate) ctap: MakeCredentialResponse, + pub(crate) transport: Vec, + pub(crate) attachment_modality: String, +} + +impl MakeCredentialResponseInternal { + pub(crate) fn new( + response: MakeCredentialResponse, + transport: Vec, + attachment_modality: String, + ) -> Self { + Self { + ctap: response, + transport, + attachment_modality, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct GetAssertionResponseInternal { + pub(crate) ctap: Assertion, + pub(crate) attachment_modality: String, +} + +impl GetAssertionResponseInternal { + pub(crate) fn new(ctap: Assertion, attachment_modality: String) -> Self { + Self { + ctap, + attachment_modality, + } + } +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict", rename_all = "camelCase")] +pub struct GetClientCapabilitiesResponse { + pub conditional_create: bool, + pub conditional_get: bool, + pub hybrid_transport: bool, + pub passkey_platform_authenticator: bool, + pub user_verifying_platform_authenticator: bool, + pub related_origins: bool, + pub signal_all_accepted_credentials: bool, + pub signal_current_user_details: bool, + pub signal_unknown_credential: bool, +} + #[derive(Debug)] pub enum CredentialType { Passkey, @@ -158,9 +244,9 @@ pub enum UsbState { // This isn't actually sent from the server. //UserCancelled, - // Multiple credentials have been found and the user has to select which to use - // List of user-identities to decide which to use. + /// Multiple credentials have been found and the user has to select which to use SelectCredential { + /// List of user-identities to decide which to use. creds: Vec, }, @@ -192,3 +278,4 @@ pub enum Error { /// Something went wrong with the credential service itself, not the authenticator. Internal(String), } + diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs index 89d568df..03c21e10 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs @@ -4,15 +4,26 @@ use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use libwebauthn::{ ops::webauthn::{CredentialProtectionPolicy, MakeCredentialLargeBlobExtension}, proto::ctap2::{ - Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, - Ctap2PublicKeyCredentialType, Ctap2Transport, + Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialType, Ctap2Transport, }, }; +use ring::digest; use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::debug; -use crate::cose::{CoseKeyAlgorithmIdentifier, CoseKeyType}; +use crate::{cose::{CoseKeyAlgorithmIdentifier, CoseKeyType}, model::Operation}; + +pub use libwebauthn::ops::webauthn::{ + Assertion, CredentialProtectionExtension, GetAssertionHmacOrPrfInput, + GetAssertionLargeBlobExtension, GetAssertionRequest, GetAssertionRequestExtensions, + MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialResponse, + MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement, +}; +pub use libwebauthn::proto::ctap2::{ + Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, + Ctap2PublicKeyCredentialUserEntity, +}; #[derive(Debug)] pub enum Error { @@ -655,3 +666,23 @@ impl GetPublicKeyCredentialResponse { output.to_string() } } + +pub fn create_client_data_hash(json: &str) -> Vec { + digest::digest(&digest::SHA256, json.as_bytes()) + .as_ref() + .to_owned() +} + +pub fn format_client_data_json( + op: Operation, + challenge: &str, + origin: &str, + is_cross_origin: bool, +) -> String { + let op_str = match op { + Operation::Create { .. } => "webauthn.create", + Operation::Get { .. } => "webauthn.get", + }; + let cross_origin_str = if is_cross_origin { "true" } else { "false" }; + format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge}\",\"origin\":\"{origin}\",\"crossOrigin\":{cross_origin_str}}}") +} From 4be39e1f2a21f9a47b993d6c7b34241a082e265a Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 29 Jul 2025 18:18:35 -0500 Subject: [PATCH 10/38] Update in-process client/server implementation with new methods --- .../src/credential_service/server.rs | 422 +++++++++++------- 1 file changed, 253 insertions(+), 169 deletions(-) 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 623df7fb..508eb068 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 @@ -1,7 +1,7 @@ use std::fmt::Debug; use std::future::Future; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use async_std::sync::Mutex as AsyncMutex; use futures_lite::{Stream, StreamExt}; @@ -13,44 +13,71 @@ use super::hybrid::{HybridHandler, HybridState}; use super::usb::{UsbHandler, UsbState}; use super::CredentialService; -#[allow(clippy::enum_variant_names)] -pub enum ServiceRequest { - GetDevices, - GetHybridCredential, - GetUsbCredential, -} - enum ManagementRequest { InitRequest(Box), CompleteAuth, + GetDevices, + GetHybridCredential, + GetUsbCredential, } -#[derive(Debug)] enum ManagementResponse { + EnterClientPin, InitRequest(Result<(), String>), - CompleteAuth(Option), + CompleteAuth(Result), + GetDevices(Vec), + GetHybridCredential, + GetUsbCredential, + InitStream(Result + Send + 'static>>, ()>), +} + +impl Debug for ManagementResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InitRequest(arg0) => f.debug_tuple("InitRequest").field(arg0).finish(), + Self::CompleteAuth(arg0) => f.debug_tuple("CompleteAuth").field(arg0).finish(), + Self::EnterClientPin => f.debug_tuple("EnterClientPin").finish(), + Self::GetDevices(arg0) => f.debug_tuple("GetDevices").field(arg0).finish(), + Self::GetHybridCredential => f.debug_tuple("GetHybridCredential").finish(), + Self::GetUsbCredential => f.debug_tuple("GetUsbCredential").finish(), + Self::InitStream(_) => f + .debug_tuple("InitStream") + .field(&String::from("")) + .finish(), + } + } +} + +#[allow(clippy::enum_variant_names)] +pub enum ServiceRequest { + EnterClientPin(String), + GetDevices, + GetHybridCredential, + GetUsbCredential, + InitStream, } // Clippy complains that these variant names have the same prefix, but that's // intentional for now. #[allow(clippy::enum_variant_names)] pub enum ServiceResponse { + EnterClientPin, GetDevices(Vec), - GetHybridCredential(Pin + Send>>), - GetUsbCredential(Pin + Send>>), + GetHybridCredential, + GetUsbCredential, + InitStream(Result + Send + 'static>>, ()>), } impl Debug for ServiceResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::EnterClientPin => f.debug_tuple("EnterClientPin").finish(), 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("")) + Self::GetHybridCredential => f.debug_tuple("GetHybridCredential").finish(), + Self::GetUsbCredential => f.debug_tuple("GetUsbCredential").finish(), + Self::InitStream(_) => f + .debug_tuple("InitStream") + .field(&String::from("")) .finish(), } } @@ -111,14 +138,41 @@ pub trait CredentialManagementClient { ) -> impl Future> + Send; } -pub struct InProcessManager { +#[derive(Debug)] +pub struct InProcessManager +where + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, +{ tx: mpsc::Sender<( InProcessServerRequest, oneshot::Sender, )>, + svc: Arc>>, + bg_event_tx: Option>, + usb_pin_tx: Arc>>>, + usb_event_forwarder_task: Arc>>>, + hybrid_event_forwarder_task: Arc>>>, +} + +impl Clone + for InProcessManager +{ + fn clone(&self) -> Self { + Self { + tx: self.tx.clone(), + svc: self.svc.clone(), + bg_event_tx: self.bg_event_tx.clone(), + usb_pin_tx: self.usb_pin_tx.clone(), + usb_event_forwarder_task: self.usb_event_forwarder_task.clone(), + hybrid_event_forwarder_task: self.hybrid_event_forwarder_task.clone(), + } + } } -impl InProcessManager { +impl + InProcessManager +{ async fn send(&self, request: ManagementRequest) -> Result { let (response_tx, response_rx) = oneshot::channel(); self.tx @@ -139,85 +193,147 @@ impl InProcessManager { } } -impl CredentialManagementClient for InProcessManager { +impl + CredentialManagementClient for InProcessManager +{ async fn init_request(&self, cred_request: CredentialRequest) -> Result<(), String> { - let response = self - .send(ManagementRequest::InitRequest(Box::new(cred_request))) - .await - .unwrap(); - if let ManagementResponse::InitRequest(result) = response { - result - } else { - Err("No credentials in credential service".to_string()) - } + self.svc.lock().await.init_request(&cred_request) } 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()) - } + self.svc + .lock() + .await + .complete_auth() + .ok_or("No credentials in credential service".to_string()) } - fn get_available_public_key_devices( - &self, - ) -> impl Future, ()>> + Send { - todo!() + async fn get_available_public_key_devices(&self) -> Result, ()> { + self.svc + .lock() + .await + .get_available_public_key_devices() + .await } - fn get_hybrid_credential(&mut self) -> impl Future> + Send { - todo!() + async fn get_hybrid_credential(&mut self) -> Result<(), ()> { + let svc = self.svc.lock().await; + let mut stream = svc.get_hybrid_credential(); + if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { + let task = async_std::task::spawn(async move { + while let Some(hybrid_state) = stream.next().await { + if let Some(tx) = tx_weak.upgrade() { + match hybrid_state { + HybridState::Completed | HybridState::Failed => { + tx.send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + .await + .unwrap(); + break; + } + _ => tx + .send(BackgroundEvent::HybridQrStateChanged(hybrid_state.into())) + .await + .unwrap(), + }; + } + } + }); + if let Some(prev_task) = self + .hybrid_event_forwarder_task + .lock() + .unwrap() + .replace(task) + { + async_std::task::block_on(prev_task.cancel()); + } + } + Ok(()) } - fn get_usb_credential(&mut self) -> impl Future> + Send { - todo!() + async fn get_usb_credential(&mut self) -> Result<(), ()> { + let mut stream = self.svc.lock().await.get_usb_credential(); + if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { + let usb_pin_tx = self.usb_pin_tx.clone(); + let task = async_std::task::spawn(async move { + while let Some(state) = stream.next().await { + if let Some(tx) = tx_weak.upgrade() { + if tx + .send(BackgroundEvent::UsbStateChanged((&state).into())) + .await + .is_err() + { + tracing::debug!("Closing USB background event forwarder"); + break; + } + match state { + UsbState::NeedsPin { pin_tx, .. } => { + let mut usb_pin_tx = usb_pin_tx.lock().await; + let _ = usb_pin_tx.insert(pin_tx); + } + UsbState::Completed | UsbState::Failed(_) => { + break; + } + _ => {} + }; + } + } + }); + if let Some(prev_task) = self.usb_event_forwarder_task.lock().unwrap().replace(task) { + async_std::task::block_on(prev_task.cancel()); + } + } + Ok(()) } - fn initiate_event_stream( + async fn initiate_event_stream( &mut self, - ) -> impl Future< - Output = Result + Send + 'static>>, ()>, - > + Send { - todo!() + ) -> Result + Send + 'static>>, ()> { + let (tx, mut rx) = mpsc::channel(32); + self.bg_event_tx = Some(tx); + Ok(Box::pin(async_stream::stream! { + // TODO: we need to add a shutdown event that tells this stream + // to shut down when completed, failed or cancelled + while let Some(bg_event) = rx.recv().await { + yield bg_event + } + tracing::debug!("event stream ended"); + })) } - fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send { - todo!() + async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { + if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { + pin_tx.send(pin).await.unwrap(); + } + Ok(()) } - fn select_credential( - &self, - credential_id: String, - ) -> impl Future> + Send { - todo!() + async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + todo!(); } } -pub struct InProcessClient { - tx: mpsc::Sender<( - InProcessServerRequest, - oneshot::Sender, - )>, - bg_event_tx: Option>, - usb_pin_tx: Arc>>>, - usb_event_forwarder_task: Option>, - hybrid_event_forwarder_task: Option>, -} - -impl Drop for InProcessClient { +impl Drop + for InProcessManager +{ fn drop(&mut self) { - if let Some(task) = self.usb_event_forwarder_task.take() { + if let Some(task) = self.usb_event_forwarder_task.lock().unwrap().take() { async_std::task::block_on(task.cancel()); } - if let Some(task) = self.hybrid_event_forwarder_task.take() { + if let Some(task) = self.hybrid_event_forwarder_task.lock().unwrap().take() { async_std::task::block_on(task.cancel()); } } } +/// Represents a client for the UI to call methods on the credential service. +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(); @@ -250,103 +366,44 @@ impl CredentialServiceClient for InProcessClient { } async fn get_hybrid_credential(&mut self) -> Result<(), ()> { - let response = self - .send(ServiceRequest::GetHybridCredential) - .await - .unwrap(); - if let ServiceResponse::GetHybridCredential(mut stream) = response { - if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { - let task = async_std::task::spawn(async move { - while let Some(hybrid_state) = stream.next().await { - if let Some(tx) = tx_weak.upgrade() { - match hybrid_state { - HybridState::Completed | HybridState::Failed => { - tx.send(BackgroundEvent::HybridQrStateChanged( - hybrid_state.into(), - )) - .await - .unwrap(); - break; - } - _ => tx - .send(BackgroundEvent::HybridQrStateChanged( - hybrid_state.into(), - )) - .await - .unwrap(), - }; - } - } - }); - if let Some(prev_task) = self.hybrid_event_forwarder_task.replace(task) { - prev_task.cancel().await; - } - } + if let Ok(ServiceResponse::GetHybridCredential) = + self.send(ServiceRequest::GetHybridCredential).await + { Ok(()) } else { - panic!("Unable to get hybrid credential"); + Err(()) } } async fn get_usb_credential(&mut self) -> Result<(), ()> { let response = self.send(ServiceRequest::GetUsbCredential).await.unwrap(); - if let ServiceResponse::GetUsbCredential(mut stream) = response { - if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { - let usb_pin_tx = self.usb_pin_tx.clone(); - let task = async_std::task::spawn(async move { - while let Some(state) = stream.next().await { - if let Some(tx) = tx_weak.upgrade() { - if tx - .send(BackgroundEvent::UsbStateChanged((&state).into())) - .await - .is_err() - { - tracing::debug!("Closing USB background event forwarder"); - break; - } - match state { - UsbState::NeedsPin { pin_tx, .. } => { - let mut usb_pin_tx = usb_pin_tx.lock().await; - let _ = usb_pin_tx.insert(pin_tx); - } - UsbState::Completed | UsbState::Failed(_) => { - break; - } - _ => {} - }; - } - } - }); - if let Some(prev_task) = self.usb_event_forwarder_task.replace(task) { - prev_task.cancel().await; - } - } + if let ServiceResponse::GetUsbCredential = response { Ok(()) } else { - panic!("Unable to get usb credential"); + Err(()) } } async fn initiate_event_stream( &mut self, ) -> Result + Send + 'static>>, ()> { - let (tx, mut rx) = mpsc::channel(32); - self.bg_event_tx = Some(tx); - Ok(Box::pin(async_stream::stream! { - // TODO: we need to add a shutdown event that tells this stream - // to shut down when completed, failed or cancelled - while let Some(bg_event) = rx.recv().await { - yield bg_event - } - tracing::debug!("event stream ended"); - })) + if let Ok(ServiceResponse::InitStream(Ok(stream))) = + self.send(ServiceRequest::InitStream).await + { + Ok(stream) + } else { + Err(()) + } } async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { - if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { - pin_tx.send(pin).await.unwrap(); + if let Ok(ServiceResponse::EnterClientPin) = + self.send(ServiceRequest::EnterClientPin(pin)).await + { + Ok(()) + } else { + Err(()) } - Ok(()) } async fn select_credential(&self, credential_id: String) -> Result<(), ()> { @@ -389,60 +446,87 @@ impl CredentialServiceClient for Arc { #[derive(Debug)] pub struct InProcessServer where - H: HybridHandler + Debug, - U: UsbHandler + Debug, + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, { - svc: CredentialService, rx: mpsc::Receiver<( InProcessServerRequest, oneshot::Sender, )>, + mgr: InProcessManager, } impl InProcessServer where - H: HybridHandler + Debug, - U: UsbHandler + Debug, + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, { - pub fn new(svc: CredentialService) -> (Self, InProcessManager, InProcessClient) { + pub fn new(svc: CredentialService) -> (Self, InProcessManager, InProcessClient) { let (tx, rx) = mpsc::channel(256); + let svc_arc = Arc::new(AsyncMutex::new(svc)); let mgr_tx = tx.clone(); - let mgr = InProcessManager { tx: mgr_tx }; - let client_tx = tx.clone(); - let client = InProcessClient { - tx: client_tx, + let mgr = InProcessManager { + tx: mgr_tx.clone(), + svc: svc_arc, bg_event_tx: None, usb_pin_tx: Arc::new(AsyncMutex::new(None)), - usb_event_forwarder_task: None, - hybrid_event_forwarder_task: None, + usb_event_forwarder_task: Arc::new(Mutex::new(None)), + hybrid_event_forwarder_task: Arc::new(Mutex::new(None)), }; - (Self { svc, rx }, mgr, client) + let client_tx = tx.clone(); + let client = InProcessClient { tx: client_tx }; + let server = Self { + rx, + mgr: mgr.clone(), + }; + (server, mgr, client) } pub async fn run(&mut self) { while let Some((request, tx)) = self.rx.recv().await { let response = match request { + InProcessServerRequest::Client(ServiceRequest::EnterClientPin(pin)) => { + let rsp = self.mgr.enter_client_pin(pin).await; + InProcessServerResponse::Client(ServiceResponse::EnterClientPin) + } InProcessServerRequest::Client(ServiceRequest::GetDevices) => { - let rsp = self.svc.get_available_public_key_devices().await.unwrap(); + let rsp = self.mgr.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)) + let rsp = self.mgr.get_hybrid_credential().await; + InProcessServerResponse::Client(ServiceResponse::GetHybridCredential) } + InProcessServerRequest::Client(ServiceRequest::GetUsbCredential) => { - let rsp = self.svc.get_usb_credential(); - InProcessServerResponse::Client(ServiceResponse::GetUsbCredential(rsp)) + let rsp = self.mgr.get_usb_credential().await; + InProcessServerResponse::Client(ServiceResponse::GetUsbCredential) + } + InProcessServerRequest::Client(ServiceRequest::InitStream) => { + let rsp = self.mgr.initiate_event_stream().await; + InProcessServerResponse::Client(ServiceResponse::InitStream(rsp)) } InProcessServerRequest::Management(ManagementRequest::InitRequest(request)) => { - let rsp = self.svc.init_request(&request); + let rsp = self.mgr.init_request(*request).await; InProcessServerResponse::Management(ManagementResponse::InitRequest(rsp)) } InProcessServerRequest::Management(ManagementRequest::CompleteAuth) => { - let rsp = self.svc.complete_auth(); + let rsp = self.mgr.complete_auth().await; InProcessServerResponse::Management(ManagementResponse::CompleteAuth(rsp)) } + InProcessServerRequest::Management(ManagementRequest::GetDevices) => { + let rsp = self.mgr.get_available_public_key_devices().await.unwrap(); + InProcessServerResponse::Management(ManagementResponse::GetDevices(rsp)) + } + InProcessServerRequest::Management(ManagementRequest::GetHybridCredential) => { + let rsp = self.mgr.get_hybrid_credential().await; + InProcessServerResponse::Management(ManagementResponse::GetHybridCredential) + } + InProcessServerRequest::Management(ManagementRequest::GetUsbCredential) => { + let rsp = self.mgr.get_usb_credential().await; + InProcessServerResponse::Client(ServiceResponse::GetUsbCredential) + } }; tx.send(response).unwrap() } From b71c1107ba6d3fefc1688804e097e169e739047d Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 29 Jul 2025 18:18:35 -0500 Subject: [PATCH 11/38] Apply lints --- .../src/dbus.rs | 87 ++++++++++++------- .../src/dbus/model.rs | 66 +++++++------- 2 files changed, 92 insertions(+), 61 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 61b0207d..84cef262 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -31,23 +31,27 @@ mod model; +use futures_lite::StreamExt; use std::collections::VecDeque; use std::sync::Arc; -use futures_lite::StreamExt; use tokio::sync::Mutex as AsyncMutex; use zbus::object_server::SignalEmitter; use zbus::zvariant; use zbus::{ connection::{self, Connection}, - fdo, interface, - Result, + fdo, interface, Result, }; use crate::credential_service::{CredentialManagementClient, CredentialServiceClient}; use crate::gui::ViewRequest; -use crate::model::{CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, Operation}; +use crate::model::{ + CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, Operation, +}; -use self::model::{BackgroundEvent, CreateCredentialResponse, Device, GetPublicKeyCredentialResponse, CreatePublicKeyCredentialResponse, GetCredentialRequest, GetCredentialResponse}; +use self::model::{ + BackgroundEvent, CreateCredentialResponse, CreatePublicKeyCredentialResponse, Device, + GetCredentialRequest, GetCredentialResponse, GetPublicKeyCredentialResponse, +}; // TODO: This is a workaround for testing credential_service. Refactor so that // these private structs don't need to be exported. pub use self::model::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}; @@ -307,12 +311,11 @@ impl InternalService { struct UiControlService; +/// These methods are called by the credential service to control the UI. #[interface(name = "xyz.iinuwa.credentials.UiControl1")] impl UiControlService { fn launch_ui(&self) {} - fn send_state_changed(&self) { - - } + fn send_state_changed(&self) {} } async fn execute_flow( @@ -354,59 +357,79 @@ async fn execute_flow( } struct DbusCredentialClient<'a> { - proxy: InternalServiceProxy<'a> + proxy: InternalServiceProxy<'a>, } impl CredentialServiceClient for DbusCredentialClient<'_> { async fn get_available_public_key_devices( &self, ) -> std::result::Result, ()> { - let dbus_devices = self.proxy.get_available_public_key_devices().await.map_err(|_|())?; + let dbus_devices = self + .proxy + .get_available_public_key_devices() + .await + .map_err(|_| ())?; dbus_devices.into_iter().map(|d| d.try_into()).collect() } - async fn get_hybrid_credential( - &mut self, - ) -> std::result::Result<(), ()> { - self.proxy.get_hybrid_credential().await + async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { + self.proxy + .get_hybrid_credential() + .await .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) .map_err(|_| ()) } - async fn get_usb_credential( - &mut self, - ) -> std::result::Result<(), ()> { - self.proxy.get_hybrid_credential().await + async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { + self.proxy + .get_hybrid_credential() + .await .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) .map_err(|_| ()) } async fn initiate_event_stream( &mut self, - ) -> std::result::Result + Send + 'static>>, ()> { - let stream = self.proxy.receive_state_changed().await + ) -> std::result::Result< + std::pin::Pin< + Box + Send + 'static>, + >, + (), + > { + let stream = self + .proxy + .receive_state_changed() + .await .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? .filter_map(|msg| { - msg.args().and_then(|args| args.update.try_into().map_err(|err: zvariant::Error| err.into())) - .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) - .ok() + msg.args() + .and_then(|args| { + args.update + .try_into() + .map_err(|err: zvariant::Error| err.into()) + }) + .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) + .ok() }) .boxed(); - self.proxy.initiate_event_stream().await - .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) - .and_then(|_| Ok(stream)) + self.proxy + .initiate_event_stream() + .await + .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) + .and_then(|_| Ok(stream)) } async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { - self.proxy.enter_client_pin(pin).await + self.proxy + .enter_client_pin(pin) + .await .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) } - async fn select_credential( - &self, - credential_id: String, - ) -> std::result::Result<(), ()> { - self.proxy.select_credential(credential_id).await + async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { + self.proxy + .select_credential(credential_id) + .await .map_err(|err| tracing::error!("Failed to select credential: {err}")) } } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs index b1843a25..60062f7b 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs @@ -8,23 +8,20 @@ use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use serde::{Deserialize, Serialize}; use zbus::{ fdo, - zvariant::{ - self, DeserializeDict, OwnedValue, SerializeDict, Type, Value, LE - }, + zvariant::{self, DeserializeDict, OwnedValue, SerializeDict, Type, Value, LE}, }; use crate::model::{ - CredentialType, GetAssertionResponseInternal, - MakeCredentialResponseInternal, Operation, ViewUpdate + CredentialType, GetAssertionResponseInternal, MakeCredentialResponseInternal, Operation, + ViewUpdate, }; use crate::webauthn::{ - self, GetPublicKeyCredentialUnsignedExtensionsResponse, PublicKeyCredentialParameters, - CredentialProtectionExtension, GetAssertionHmacOrPrfInput, - GetAssertionLargeBlobExtension, GetAssertionRequest, GetAssertionRequestExtensions, - MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement, - - Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, - Ctap2PublicKeyCredentialUserEntity, + self, CredentialProtectionExtension, Ctap2PublicKeyCredentialDescriptor, + Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, + GetAssertionHmacOrPrfInput, GetAssertionLargeBlobExtension, GetAssertionRequest, + GetAssertionRequestExtensions, GetPublicKeyCredentialUnsignedExtensionsResponse, + MakeCredentialHmacOrPrfInput, MakeCredentialRequest, MakeCredentialsRequestExtensions, + PublicKeyCredentialParameters, ResidentKeyRequirement, UserVerificationRequirement, }; // D-Bus <-> Client types @@ -437,7 +434,6 @@ pub struct GetPublicKeyCredentialRequest { pub(crate) request_json: String, } - #[derive(SerializeDict, Type)] #[zvariant(signature = "dict")] pub struct GetCredentialResponse { @@ -580,9 +576,7 @@ impl TryFrom for ViewUpdate { }), ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), - ClientUpdate::HybridNeedsQrCode(v) => v - .try_into() - .map(Self::HybridNeedsQrCode), + ClientUpdate::HybridNeedsQrCode(v) => v.try_into().map(Self::HybridNeedsQrCode), ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), @@ -737,10 +731,11 @@ impl From for crate::model::Error { ServiceError::PinAttemptsExhausted => Self::PinAttemptsExhausted, // TODO: this is bogus, we should refactor to remove the tuple field // and let the client decide how to render the error. - ServiceError::Internal => Self::Internal("Something went wrong. Please try again later.".to_string()), + ServiceError::Internal => { + Self::Internal("Something went wrong. Please try again later.".to_string()) + } } } - } /// Used to de-/serialize state D-Bus and model::UsbState. @@ -776,22 +771,36 @@ impl TryFrom for crate::model::UsbState { UsbState::Waiting(_) => Ok(Self::Waiting), UsbState::SelectingDevice(_) => Ok(Self::SelectingDevice), UsbState::Connected(_) => Ok(Self::Connected), - UsbState::NeedsPin(value) => value.try_into().map(|attempts_left| { - let attempts_left = if attempts_left < 0 { None } else { Some(attempts_left) }; + UsbState::NeedsPin(value) => value.try_into().map(|attempts_left: i32| { + let attempts_left = if attempts_left < 0 { + None + } else { + Some(u32::try_from(attempts_left).unwrap()) + }; Self::NeedsPin { attempts_left } }), - UsbState::NeedsUserVerification(value) => value.try_into().map(|attempts_left| { - let attempts_left = if attempts_left < 0 { None } else { Some(attempts_left) }; + UsbState::NeedsUserVerification(value) => value.try_into().map(|attempts_left: i32| { + let attempts_left = if attempts_left < 0 { + None + } else { + Some(u32::try_from(attempts_left).unwrap()) + }; Self::NeedsUserVerification { attempts_left } }), UsbState::NeedsUserPresence(_) => Ok(Self::NeedsUserPresence), - UsbState::SelectCredential(value) => { - value.try_into() - .map(|creds: Vec| creds.into_iter().map(crate::model::Credential::from).collect()) - .map(|creds| Self::SelectCredential { creds }) - }, + UsbState::SelectCredential(value) => value + .try_into() + .map(|creds: Vec| { + creds + .into_iter() + .map(crate::model::Credential::from) + .collect() + }) + .map(|creds| Self::SelectCredential { creds }), UsbState::Completed(_) => Ok(Self::Completed), - UsbState::Failed(value) => ServiceError::try_from(Value::<'_>::from(value)).map(|err| Self::Failed(err.into())), + UsbState::Failed(value) => { + ServiceError::try_from(Value::<'_>::from(value)).map(|err| Self::Failed(err.into())) + } }?; Ok(ret) } @@ -806,4 +815,3 @@ impl TryFrom> for UsbState { Ok(obj) } } - From 7fb26795d1d0ed2f83a822d5d0199b079f29e3d1 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 30 Jul 2025 17:19:17 -0500 Subject: [PATCH 12/38] Fix up D-Bus UI-cred service client --- .../src/credential_service/server.rs | 2 + .../src/dbus.rs | 38 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) 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 508eb068..4038aed0 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 @@ -94,6 +94,7 @@ enum InProcessServerResponse { Management(ManagementResponse), } +/// Used for communication from trusted UI to credential service pub trait CredentialServiceClient { fn get_available_public_key_devices( &self, @@ -113,6 +114,7 @@ pub trait CredentialServiceClient { ) -> impl Future> + Send; } +/// Used for communication from privileged broker to credential service pub trait CredentialManagementClient { fn init_request( &self, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 84cef262..c21e7b15 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -356,16 +356,28 @@ async fn execute_flow( }) } -struct DbusCredentialClient<'a> { - proxy: InternalServiceProxy<'a>, +pub struct DbusCredentialClient { + conn: Connection, } -impl CredentialServiceClient for DbusCredentialClient<'_> { +impl DbusCredentialClient { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + async fn proxy(&self) -> std::result::Result { + InternalServiceProxy::new(&self.conn) + .await + .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) + } +} + +impl CredentialServiceClient for DbusCredentialClient { async fn get_available_public_key_devices( &self, ) -> std::result::Result, ()> { let dbus_devices = self - .proxy + .proxy() + .await? .get_available_public_key_devices() .await .map_err(|_| ())?; @@ -373,7 +385,8 @@ impl CredentialServiceClient for DbusCredentialClient<'_> { } async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { - self.proxy + self.proxy() + .await? .get_hybrid_credential() .await .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) @@ -381,7 +394,8 @@ impl CredentialServiceClient for DbusCredentialClient<'_> { } async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { - self.proxy + self.proxy() + .await? .get_hybrid_credential() .await .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) @@ -397,7 +411,8 @@ impl CredentialServiceClient for DbusCredentialClient<'_> { (), > { let stream = self - .proxy + .proxy() + .await? .receive_state_changed() .await .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? @@ -412,7 +427,8 @@ impl CredentialServiceClient for DbusCredentialClient<'_> { .ok() }) .boxed(); - self.proxy + self.proxy() + .await? .initiate_event_stream() .await .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) @@ -420,14 +436,16 @@ impl CredentialServiceClient for DbusCredentialClient<'_> { } async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { - self.proxy + self.proxy() + .await? .enter_client_pin(pin) .await .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) } async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { - self.proxy + self.proxy() + .await? .select_credential(credential_id) .await .map_err(|err| tracing::error!("Failed to select credential: {err}")) From 8b5b6c3512c770c5b0967130035ca3635bb5b374 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 30 Jul 2025 17:19:17 -0500 Subject: [PATCH 13/38] wip: Attempt to wire up D-Bus client for UI This doesn't work because of the mixing of Tokio and async-std runtimes. :( --- .../src/dbus.rs | 1 - .../src/gui/mod.rs | 23 +++++++++++++------ .../src/main.rs | 8 ------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index c21e7b15..747db979 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -43,7 +43,6 @@ use zbus::{ }; use crate::credential_service::{CredentialManagementClient, CredentialServiceClient}; -use crate::gui::ViewRequest; use crate::model::{ CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, Operation, }; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs index 4f19b641..fefd0dfa 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs @@ -16,23 +16,32 @@ pub struct ViewRequest { pub signal: oneshot::Sender<()>, } -pub(super) fn start_gui_thread( +pub(super) fn start_gui_thread( rx: Receiver, client: C, ) { thread::Builder::new() .name("gui".into()) .spawn(move || { - let client = Arc::new(AsyncMutex::new(client)); - // D-Bus received a request and needs a window open - while let Ok(view_request) = rx.recv_blocking() { - run_gui(client.clone(), view_request); - } + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(async move { + let client = Arc::new(AsyncMutex::new(client)); + // 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: Arc>, request: ViewRequest) { +fn run_gui( + client: Arc>, + request: ViewRequest, +) { let ViewRequest { operation, signal: response_tx, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index eac64eb3..9eb37de0 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -4,7 +4,6 @@ mod config; mod cose; mod credential_service; mod dbus; -mod gui; mod model; mod serde; mod webauthn; @@ -37,13 +36,6 @@ async fn run() -> Result<(), Box> { }); println!(" ✅"); - 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!(" ✅"); - print!("Starting D-Bus service..."); let service_name = "xyz.iinuwa.credentials.CredentialManagerUi"; let path = "/xyz/iinuwa/credentials/CredentialManagerUi"; From 892364faa14433298ddca41ffe72b2b2a669cc62 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 30 Jul 2025 17:19:17 -0500 Subject: [PATCH 14/38] Move shared types to model from gui --- .../src/dbus.rs | 3 +- .../src/gui/mod.rs | 7 +---- .../src/gui/view_model/mod.rs | 17 ---------- .../src/model.rs | 31 ++++++++++++++++--- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 747db979..2f9f2355 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -44,7 +44,8 @@ use zbus::{ use crate::credential_service::{CredentialManagementClient, CredentialServiceClient}; use crate::model::{ - CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, Operation, + CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, + Operation, ViewRequest, }; use self::model::{ diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs index fefd0dfa..26d80c4f 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs @@ -7,15 +7,10 @@ use async_std::{channel::Receiver, sync::Mutex as AsyncMutex}; use tokio::sync::oneshot; use crate::credential_service::CredentialServiceClient; -use crate::model::{Operation, ViewUpdate}; +use crate::model::{Operation, ViewRequest, ViewUpdate}; use view_model::ViewEvent; -pub struct ViewRequest { - pub operation: Operation, - pub signal: oneshot::Sender<()>, -} - pub(super) fn start_gui_thread( rx: Receiver, client: C, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs index 5b22c762..aa8161f8 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/gui/view_model/mod.rs @@ -282,20 +282,3 @@ pub enum Event { Background(BackgroundEvent), View(ViewEvent), } - -impl From for HybridState { - fn from(value: crate::credential_service::hybrid::HybridState) -> Self { - match value { - crate::credential_service::hybrid::HybridState::Init(qr_code) => { - 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 - } - crate::credential_service::hybrid::HybridState::Failed => HybridState::Failed, - } - } -} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs index 7eed6def..3a0698ff 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs @@ -1,7 +1,15 @@ use serde::{Deserialize, Serialize}; -use zbus:: zvariant::{SerializeDict, Type}; +use tokio::sync::oneshot; +use zbus::zvariant::{SerializeDict, Type}; -use crate::webauthn::{Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse}; +use crate::webauthn::{ + Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse, +}; + +pub struct ViewRequest { + pub operation: Operation, + pub signal: oneshot::Sender<()>, +} #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Credential { @@ -211,6 +219,23 @@ pub enum HybridState { Failed, } +impl From for HybridState { + fn from(value: crate::credential_service::hybrid::HybridState) -> Self { + match value { + crate::credential_service::hybrid::HybridState::Init(qr_code) => { + 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 + } + crate::credential_service::hybrid::HybridState::Failed => HybridState::Failed, + } + } +} + /// Used to share public state between credential service and UI. #[derive(Clone, Debug, Default)] pub enum UsbState { @@ -243,7 +268,6 @@ pub enum UsbState { // TODO: implement cancellation // This isn't actually sent from the server. //UserCancelled, - /// Multiple credentials have been found and the user has to select which to use SelectCredential { /// List of user-identities to decide which to use. @@ -278,4 +302,3 @@ pub enum Error { /// Something went wrong with the credential service itself, not the authenticator. Internal(String), } - From b191109c0311d9daffdef1e0954eb6a45262ea52 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 30 Jul 2025 17:19:17 -0500 Subject: [PATCH 15/38] Move GUI to separate Cargo project --- creds-ui/.gitignore | 1 + creds-ui/Cargo.lock | 1439 +++++++++++++++++ creds-ui/Cargo.toml | 12 + .../icons/check-round-outline-symbolic.svg | 0 .../data/icons/dialpad-symbolic.svg | 0 .../data/icons/fingerprint-symbolic.svg | 0 .../data/icons/meson.build | 0 .../data/icons/symbolic-link-symbolic.svg | 0 ...yz.iinuwa.CredentialManagerUi-symbolic.svg | 0 .../xyz.iinuwa.CredentialManagerUi.Devel.svg | 0 .../icons/xyz.iinuwa.CredentialManagerUi.svg | 0 .../data/meson.build | 44 +- .../data/resources/meson.build | 0 .../data/resources/resources.gresource.xml | 0 .../data/resources/style.css | 0 .../data/resources/ui/shortcuts.ui | 0 .../data/resources/ui/window.ui | 0 ...z.iinuwa.CredentialManagerUi.desktop.in.in | 0 ....iinuwa.CredentialManagerUi.gschema.xml.in | 0 ...uwa.CredentialManagerUi.metainfo.xml.in.in | 0 creds-ui/meson.build | 74 + .../po/LINGUAS | 0 .../po/POTFILES.in | 0 .../po/meson.build | 0 .../src/config.rs.in | 0 .../src/gui/mod.rs | 16 +- .../src/gui/view_model/gtk/application.rs | 0 .../src/gui/view_model/gtk/credential.rs | 0 .../src/gui/view_model/gtk/device.rs | 0 .../src/gui/view_model/gtk/mod.rs | 0 .../src/gui/view_model/gtk/window.rs | 0 .../src/gui/view_model/mod.rs | 0 creds-ui/src/main.rs | 20 + creds-ui/src/meson.build | 75 + meson.build | 7 +- .../.gitignore | 3 +- .../Cargo.lock | 737 +-------- .../Cargo.toml | 9 +- .../meson.build | 45 +- .../src/credential_service/mod.rs | 7 +- .../src/credential_service/server.rs | 27 +- .../src/dbus.rs | 39 +- .../src/main.rs | 4 +- .../src/meson.build | 43 +- .../tests/dbus.rs | 2 +- .../tests/meson.build | 8 +- 46 files changed, 1759 insertions(+), 853 deletions(-) create mode 100644 creds-ui/.gitignore create mode 100644 creds-ui/Cargo.lock create mode 100644 creds-ui/Cargo.toml rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/check-round-outline-symbolic.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/dialpad-symbolic.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/fingerprint-symbolic.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/meson.build (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/symbolic-link-symbolic.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/xyz.iinuwa.CredentialManagerUi-symbolic.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/xyz.iinuwa.CredentialManagerUi.Devel.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/icons/xyz.iinuwa.CredentialManagerUi.svg (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/meson.build (64%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/resources/meson.build (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/resources/resources.gresource.xml (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/resources/style.css (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/resources/ui/shortcuts.ui (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/resources/ui/window.ui (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/xyz.iinuwa.CredentialManagerUi.gschema.xml.in (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/data/xyz.iinuwa.CredentialManagerUi.metainfo.xml.in.in (100%) create mode 100644 creds-ui/meson.build rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/po/LINGUAS (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/po/POTFILES.in (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/po/meson.build (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/config.rs.in (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/mod.rs (83%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/view_model/gtk/application.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/view_model/gtk/credential.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/view_model/gtk/device.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/view_model/gtk/mod.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/view_model/gtk/window.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => creds-ui}/src/gui/view_model/mod.rs (100%) create mode 100644 creds-ui/src/main.rs create mode 100644 creds-ui/src/meson.build diff --git a/creds-ui/.gitignore b/creds-ui/.gitignore new file mode 100644 index 00000000..f0c26c5c --- /dev/null +++ b/creds-ui/.gitignore @@ -0,0 +1 @@ +src/config.rs diff --git a/creds-ui/Cargo.lock b/creds-ui/Cargo.lock new file mode 100644 index 00000000..6ae8fae7 --- /dev/null +++ b/creds-ui/Cargo.lock @@ -0,0 +1,1439 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cairo-rs" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "demo-ui" +version = "0.1.0" +dependencies = [ + "async-std", + "gettext-rs", + "gtk4", + "qrcode", + "serde", + "tracing", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gettext-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44e92f7dc08430aca7ed55de161253a22276dfd69c5526e5c5e95d1f7cf338a" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb45773f5b8945f12aecd04558f545964f943dacda1b1155b3d738f5469ef661" +dependencies = [ + "cc", + "temp-dir", +] + +[[package]] +name = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pango" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + +[[package]] +name = "temp-dir" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] diff --git a/creds-ui/Cargo.toml b/creds-ui/Cargo.toml new file mode 100644 index 00000000..7745f537 --- /dev/null +++ b/creds-ui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "demo-ui" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-std = "1.13.1" +gettext-rs = { version = "0.7", features = ["gettext-system"] } +gtk = { version = "0.9.6", package = "gtk4", features = ["v4_6"] } +qrcode = "0.14.1" +serde = { version = "1.0.219", features = ["derive"] } +tracing = "0.1.41" diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/check-round-outline-symbolic.svg b/creds-ui/data/icons/check-round-outline-symbolic.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/check-round-outline-symbolic.svg rename to creds-ui/data/icons/check-round-outline-symbolic.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/dialpad-symbolic.svg b/creds-ui/data/icons/dialpad-symbolic.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/dialpad-symbolic.svg rename to creds-ui/data/icons/dialpad-symbolic.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/fingerprint-symbolic.svg b/creds-ui/data/icons/fingerprint-symbolic.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/fingerprint-symbolic.svg rename to creds-ui/data/icons/fingerprint-symbolic.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/meson.build b/creds-ui/data/icons/meson.build similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/meson.build rename to creds-ui/data/icons/meson.build diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/symbolic-link-symbolic.svg b/creds-ui/data/icons/symbolic-link-symbolic.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/symbolic-link-symbolic.svg rename to creds-ui/data/icons/symbolic-link-symbolic.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/xyz.iinuwa.CredentialManagerUi-symbolic.svg b/creds-ui/data/icons/xyz.iinuwa.CredentialManagerUi-symbolic.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/xyz.iinuwa.CredentialManagerUi-symbolic.svg rename to creds-ui/data/icons/xyz.iinuwa.CredentialManagerUi-symbolic.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/xyz.iinuwa.CredentialManagerUi.Devel.svg b/creds-ui/data/icons/xyz.iinuwa.CredentialManagerUi.Devel.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/xyz.iinuwa.CredentialManagerUi.Devel.svg rename to creds-ui/data/icons/xyz.iinuwa.CredentialManagerUi.Devel.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/icons/xyz.iinuwa.CredentialManagerUi.svg b/creds-ui/data/icons/xyz.iinuwa.CredentialManagerUi.svg similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/icons/xyz.iinuwa.CredentialManagerUi.svg rename to creds-ui/data/icons/xyz.iinuwa.CredentialManagerUi.svg diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/meson.build b/creds-ui/data/meson.build similarity index 64% rename from xyz-iinuwa-credential-manager-portal-gtk/data/meson.build rename to creds-ui/data/meson.build index ead7b4c9..f7c54e01 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/data/meson.build +++ b/creds-ui/data/meson.build @@ -8,21 +8,19 @@ desktop_file = i18n.merge_file( input: configure_file( input: '@0@.desktop.in.in'.format(base_id), output: '@BASENAME@', - configuration: desktop_conf + configuration: desktop_conf, ), output: '@0@.desktop'.format(application_id), po_dir: podir, install: true, - install_dir: datadir / 'applications' + install_dir: datadir / 'applications', ) # Validate Desktop file if desktop_file_validate.found() test( 'validate-desktop', desktop_file_validate, - args: [ - desktop_file.full_path() - ], + args: [desktop_file.full_path()], depends: desktop_file, ) endif @@ -35,20 +33,19 @@ appdata_file = i18n.merge_file( input: configure_file( input: '@0@.metainfo.xml.in.in'.format(base_id), output: '@BASENAME@', - configuration: appdata_conf + configuration: appdata_conf, ), output: '@0@.metainfo.xml'.format(application_id), po_dir: podir, install: true, - install_dir: datadir / 'metainfo' + install_dir: datadir / 'metainfo', ) # Validate Appdata if appstreamcli.found() test( - 'validate-appdata', appstreamcli, - args: [ - 'validate', '--no-net', '--explain', appdata_file.full_path() - ], + 'validate-appdata', + appstreamcli, + args: ['validate', '--no-net', '--explain', appdata_file.full_path()], depends: appdata_file, ) endif @@ -62,25 +59,26 @@ gschema_xml = configure_file( output: '@0@.gschema.xml'.format(application_id), configuration: gschema_conf, install: true, - install_dir: datadir / 'glib-2.0' / 'schemas' + install_dir: datadir / 'glib-2.0' / 'schemas', ) # Validata GSchema if glib_compile_schemas.found() test( - 'validate-gschema', glib_compile_schemas, - args: [ - '--strict', '--dry-run', meson.current_build_dir() - ], + 'validate-gschema', + glib_compile_schemas, + args: ['--strict', '--dry-run', meson.current_build_dir()], ) endif if get_option('profile') == 'development' - custom_target('gschema', - input : gschema_xml, - output : 'gschema.compiled', - command : [glib_compile_schemas, '--strict', meson.current_build_dir()], - install: false, - build_by_default : true, - build_always_stale : true) + custom_target( + 'gschema', + input: gschema_xml, + output: 'gschema.compiled', + command: [glib_compile_schemas, '--strict', meson.current_build_dir()], + install: false, + build_by_default: true, + build_always_stale: true, + ) endif diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/resources/meson.build b/creds-ui/data/resources/meson.build similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/resources/meson.build rename to creds-ui/data/resources/meson.build diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/resources/resources.gresource.xml b/creds-ui/data/resources/resources.gresource.xml similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/resources/resources.gresource.xml rename to creds-ui/data/resources/resources.gresource.xml diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/resources/style.css b/creds-ui/data/resources/style.css similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/resources/style.css rename to creds-ui/data/resources/style.css diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/resources/ui/shortcuts.ui b/creds-ui/data/resources/ui/shortcuts.ui similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/resources/ui/shortcuts.ui rename to creds-ui/data/resources/ui/shortcuts.ui diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/resources/ui/window.ui b/creds-ui/data/resources/ui/window.ui similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/resources/ui/window.ui rename to creds-ui/data/resources/ui/window.ui diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in b/creds-ui/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in rename to creds-ui/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/xyz.iinuwa.CredentialManagerUi.gschema.xml.in b/creds-ui/data/xyz.iinuwa.CredentialManagerUi.gschema.xml.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/xyz.iinuwa.CredentialManagerUi.gschema.xml.in rename to creds-ui/data/xyz.iinuwa.CredentialManagerUi.gschema.xml.in diff --git a/xyz-iinuwa-credential-manager-portal-gtk/data/xyz.iinuwa.CredentialManagerUi.metainfo.xml.in.in b/creds-ui/data/xyz.iinuwa.CredentialManagerUi.metainfo.xml.in.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/data/xyz.iinuwa.CredentialManagerUi.metainfo.xml.in.in rename to creds-ui/data/xyz.iinuwa.CredentialManagerUi.metainfo.xml.in.in diff --git a/creds-ui/meson.build b/creds-ui/meson.build new file mode 100644 index 00000000..e9984819 --- /dev/null +++ b/creds-ui/meson.build @@ -0,0 +1,74 @@ +i18n = import('i18n') +gnome = import('gnome') + +gui_executable_name = 'xyzii-credman-portal-gtk' +gui_build_dir = meson.current_build_dir() +gui_source_dir = meson.current_source_dir() +base_id = 'xyz.iinuwa.CredentialManagerUi' + +dependency('dbus-1', version: '>= 1.6') +dependency('glib-2.0', version: '>= 2.66') +dependency('gio-2.0', version: '>= 2.66') +dependency('gtk4', version: '>= 4.6.2') + +glib_compile_resources = find_program('glib-compile-resources', required: true) +glib_compile_schemas = find_program('glib-compile-schemas', required: true) +# Usually provided by gettext package +msgfmt = find_program('msgfmt', required: false) +xmllint = find_program('xmllint', required: false) + +desktop_file_validate = find_program('desktop-file-validate', required: false) +appstreamcli = find_program('appstreamcli', required: false) + +cargo = find_program('cargo', required: true) + +version = meson.project_version() + +prefix = get_option('prefix') +bindir = prefix / get_option('bindir') +localedir = prefix / get_option('localedir') + +datadir = prefix / get_option('datadir') +pkgdatadir = datadir / gui_executable_name +iconsdir = datadir / 'icons' +podir = meson.project_source_root() / meson.current_source_dir() / 'po' +gettext_package = gui_executable_name + +if get_option('profile') == 'development' + profile = 'Devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() + if vcs_tag == '' + version_suffix = '-devel' + else + version_suffix = '-@0@'.format(vcs_tag) + endif + application_id = '@0@.@1@'.format(base_id, profile) +else + profile = '' + version_suffix = '' + application_id = base_id +endif + +meson.add_dist_script( + meson.project_source_root() / 'build-aux/dist-vendor.sh', + meson.project_build_root() / 'meson-dist' / gui_executable_name + + '-' + + version, + meson.project_source_root(), +) + +if get_option('profile') == 'development' + # Setup pre-commit hook for ensuring coding style is always consistent + message('Setting up git pre-commit hook..') + run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit', check: false) +endif + +subdir('data') +subdir('po') +subdir('src') + +gnome.post_install( + gtk_update_icon_cache: true, + glib_compile_schemas: true, + update_desktop_database: true, +) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/po/LINGUAS b/creds-ui/po/LINGUAS similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/po/LINGUAS rename to creds-ui/po/LINGUAS diff --git a/xyz-iinuwa-credential-manager-portal-gtk/po/POTFILES.in b/creds-ui/po/POTFILES.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/po/POTFILES.in rename to creds-ui/po/POTFILES.in diff --git a/xyz-iinuwa-credential-manager-portal-gtk/po/meson.build b/creds-ui/po/meson.build similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/po/meson.build rename to creds-ui/po/meson.build diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/config.rs.in b/creds-ui/src/config.rs.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/config.rs.in rename to creds-ui/src/config.rs.in diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs b/creds-ui/src/gui/mod.rs similarity index 83% rename from xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs rename to creds-ui/src/gui/mod.rs index 26d80c4f..cdcd1698 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/gui/mod.rs +++ b/creds-ui/src/gui/mod.rs @@ -18,17 +18,11 @@ pub(super) fn start_gui_thread"] edition = "2021" @@ -8,10 +8,7 @@ edition = "2021" lto = true [dependencies] -async-std = { version = "1.13.1", features = ["unstable"] } base64 = "0.22.1" -gettext-rs = { version = "0.7", features = ["gettext-system"] } -gtk = { version = "0.9.6", package = "gtk4", features = ["v4_6"] } openssl = "0.10.72" ring = "0.17.14" serde = { version = "1.0.219", features = ["derive"] } @@ -25,8 +22,10 @@ async-trait = "0.1.88" tokio = { version = "1.45.0", features = ["rt-multi-thread"] } futures-lite = "2.6.0" -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" + +[dev-dependencies] +gio = "0.21.0" diff --git a/xyz-iinuwa-credential-manager-portal-gtk/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/meson.build index 50339cdc..7d8d4ad1 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/meson.build @@ -1,7 +1,5 @@ -i18n = import('i18n') -gnome = import('gnome') - -backend_executable_name = 'xyz-iinuwa-credential-manager-portal-gtk' +backend_executable_name = 'xyzii-credman' +backend_build_dir = meson.current_build_dir() base_id = 'xyz.iinuwa.CredentialManagerUi' dependency('dbus-1', version: '>= 1.6') @@ -11,30 +9,10 @@ dependency('gtk4', version: '>= 4.6.2') dependency('ssl', 'openssl', version: '>= 3.0') dependency('udev', version: '>= 249') - -glib_compile_resources = find_program('glib-compile-resources', required: true) -glib_compile_schemas = find_program('glib-compile-schemas', required: true) -# Usually provided by gettext package -msgfmt = find_program('msgfmt', required: false) -xmllint = find_program('xmllint', required: false) - -desktop_file_validate = find_program('desktop-file-validate', required: false) -appstreamcli = find_program('appstreamcli', required: false) - cargo = find_program('cargo', required: true) version = meson.project_version() -prefix = get_option('prefix') -bindir = prefix / get_option('bindir') -localedir = prefix / get_option('localedir') - -datadir = prefix / get_option('datadir') -pkgdatadir = datadir / backend_executable_name -iconsdir = datadir / 'icons' -podir = meson.project_source_root() / backend_executable_name / 'po' -gettext_package = backend_executable_name - if get_option('profile') == 'development' profile = 'Devel' vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() @@ -52,8 +30,10 @@ endif meson.add_dist_script( meson.project_source_root() / 'build-aux/dist-vendor.sh', - meson.project_build_root() / 'meson-dist' / backend_executable_name + '-' + version, - meson.project_source_root() + meson.project_build_root() / 'meson-dist' / backend_executable_name + + '-' + + version, + meson.project_source_root(), ) if get_option('profile') == 'development' @@ -62,13 +42,10 @@ if get_option('profile') == 'development' run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit', check: false) endif -subdir('data') -subdir('po') +cargo_options = [ + '--manifest-path', meson.project_source_root() / meson.current_source_dir() / 'Cargo.toml', +] +cargo_options += ['--target-dir', meson.project_build_root() / meson.current_build_dir()] + subdir('src') subdir('tests') - -gnome.post_install( - gtk_update_icon_cache: true, - glib_compile_schemas: true, - update_desktop_database: true, -) 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 0c64fd4d..08d84b7b 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 @@ -209,7 +209,7 @@ impl From for AuthenticatorResponse { mod test { use std::sync::Arc; - use async_std::stream::StreamExt; + use futures_lite::stream::StreamExt; use crate::{ credential_service::usb::InProcessUsbHandler, @@ -237,7 +237,10 @@ mod test { 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 {} }); + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(async move { while let Some(_) = stream.next().await {} }); let cred_service = Arc::try_unwrap(cred_service).unwrap(); assert!(cred_service.complete_auth().is_some()); } 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 4038aed0..53bc0599 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 @@ -3,9 +3,8 @@ use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; -use async_std::sync::Mutex as AsyncMutex; use futures_lite::{Stream, StreamExt}; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; use crate::model::{BackgroundEvent, CredentialRequest, CredentialResponse, Device}; @@ -153,8 +152,8 @@ where svc: Arc>>, bg_event_tx: Option>, usb_pin_tx: Arc>>>, - usb_event_forwarder_task: Arc>>>, - hybrid_event_forwarder_task: Arc>>>, + usb_event_forwarder_task: Arc>>, + hybrid_event_forwarder_task: Arc>>, } impl Clone @@ -222,7 +221,7 @@ impl( service_name: &str, path: &str, - gui_tx: async_std::channel::Sender, + // gui_tx: Sender, manager_client: C, ) -> Result { - let lock: Arc>> = - Arc::new(AsyncMutex::new(gui_tx)); + let lock = Arc::new(AsyncMutex::new(())); connection::Builder::session()? .name(service_name)? .serve_at( @@ -88,7 +88,8 @@ enum SignalState { } struct CredentialManager { - app_lock: Arc>>, + // app_lock: Arc>>, + app_lock: Arc>, manager_client: C, } @@ -120,7 +121,8 @@ impl CredentialManager let cred_request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); - let response = execute_flow(&tx, &self.manager_client, &cred_request).await?; + let response = + execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response @@ -181,7 +183,8 @@ impl CredentialManager let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); - let response = execute_flow(&tx, &self.manager_client, &cred_request).await?; + let response = + execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; match response { CredentialResponse::GetPublicKeyCredentialResponse(cred_response) => { @@ -309,17 +312,30 @@ impl InternalService { ) -> zbus::Result<()>; } -struct UiControlService; +struct UiControlServiceImpl; /// These methods are called by the credential service to control the UI. -#[interface(name = "xyz.iinuwa.credentials.UiControl1")] -impl UiControlService { +#[interface( + name = "xyz.iinuwa.credentials.UiControl1", + proxy( + gen_blocking = false, + default_path = "/xyz/iinuwa/credentials/UiControl", + default_service = "xyz.iinuwa.credentials.UiControl", + ) +)] +impl UiControlService for UiControlServiceImpl { fn launch_ui(&self) {} fn send_state_changed(&self) {} } +trait UiControlService { + fn launch_ui(&self); + fn send_state_changed(&self); +} + async fn execute_flow( - gui_tx: &async_std::channel::Sender, + // TODO: Replace this with UiControlClient + // gui_tx: &async_std::channel::Sender, manager_client: &C, cred_request: &CredentialRequest, ) -> Result { @@ -342,7 +358,8 @@ async fn execute_flow( operation, signal: signal_tx, }; - gui_tx.send(view_request).await.unwrap(); + // TODO: Replace this with a UiControlClient + // gui_tx.send(view_request).await.unwrap(); // wait for gui to complete signal_rx.await.map_err(|_| { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index 9eb37de0..e7d31e7c 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -1,6 +1,4 @@ mod cbor; -#[rustfmt::skip] -mod config; mod cose; mod credential_service; mod dbus; @@ -39,7 +37,7 @@ async fn run() -> Result<(), Box> { 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, dbus_to_gui_tx, cred_mgr).await?; + let _conn = dbus::start_service(service_name, path, /* dbus_to_gui_tx, */ cred_mgr).await?; println!(" ✅"); println!("Waiting for messages..."); loop { diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build index b9bb0a5c..062af136 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build @@ -1,33 +1,5 @@ -global_conf = configuration_data() -global_conf.set_quoted('APP_ID', application_id) -if (get_option('profile') == 'development') - global_conf.set_quoted('PKGDATADIR', meson.project_build_root() / backend_executable_name / 'data' / 'resources') -else - global_conf.set_quoted('PKGDATADIR', pkgdatadir) -endif -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) -configure_file( - input: 'config.rs.in', - output: 'config.rs', - configuration: global_conf -) - -# Copy the config.rs output to the source directory. -run_command( - 'cp', - meson.project_build_root() / backend_executable_name / 'src' / 'config.rs', - meson.project_source_root() / backend_executable_name / 'src' / 'config.rs', - check: true -) - -cargo_options = [ '--manifest-path', meson.project_source_root() / backend_executable_name / 'Cargo.toml' ] -cargo_options += [ '--target-dir', meson.project_build_root() / backend_executable_name / 'src' ] - if get_option('profile') == 'default' - cargo_options += [ '--release' ] + cargo_options += ['--release'] rust_target = 'release' message('Building in release mode') else @@ -35,7 +7,8 @@ else message('Building in debug mode') endif -cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] +cargo_env = ['CARGO_HOME=' + meson.project_build_root() / 'cargo-home'] +message('@0@'.format(cargo_options)) custom_target( 'cargo-build', @@ -45,15 +18,17 @@ custom_target( console: true, install: true, install_dir: bindir, - depends: resources, command: [ 'env', cargo_env, - cargo, 'build', + cargo, + 'build', cargo_options, '&&', - 'cp', backend_executable_name / 'src' / rust_target / backend_executable_name, '@OUTPUT@', - ] + 'cp', + backend_build_dir / rust_target / backend_executable_name, + '@OUTPUT@', + ], ) test( diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs index 2e7fe811..eab5bb63 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs @@ -36,7 +36,7 @@ fn test_client_capabilities() { mod client { use crate::config::{INTERFACE, PATH, SERVICE_DIR, SERVICE_NAME}; - use gtk::gio::{TestDBus, TestDBusFlags}; + use gio::{TestDBus, TestDBusFlags}; use serde::Serialize; use zbus::{blocking::Connection, zvariant::DynamicType, Message}; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build index cf289d90..390066e0 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build @@ -1,11 +1,11 @@ test_config = configuration_data() test_config.set_quoted( 'SERVICE_DIR', - meson.project_build_root() / backend_executable_name / 'tests', + meson.project_build_root() / meson.current_build_dir(), ) test_config.set( 'DBUS_EXECUTABLE', - meson.project_build_root() / backend_executable_name / 'src' / backend_executable_name, + meson.project_build_root() / backend_build_dir / backend_executable_name, ) configure_file( input: 'config' / 'mod.rs.in', @@ -16,8 +16,8 @@ configure_file( # Copy the config output to the source directory. run_command( 'cp', - meson.project_build_root() / backend_executable_name / 'tests' / 'config.rs', - meson.project_source_root() / backend_executable_name / 'tests' / 'config' / 'mod.rs', + meson.project_build_root() / meson.current_build_dir() / 'config.rs', + meson.project_source_root() / meson.current_source_dir() / 'config' / 'mod.rs', check: true, ) From 378caa0e5ac6d8b97531a54cbb8bfa3761b92420 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 31 Jul 2025 15:22:25 -0500 Subject: [PATCH 16/38] Extract shared types into common library --- .vscode/launch.json | 2 +- creds-lib/Cargo.lock | 3523 ++++++++++++++++ creds-lib/Cargo.toml | 10 + creds-lib/meson.build | 27 + creds-lib/src/client.rs | 25 + creds-lib/src/lib.rs | 3 + creds-lib/src/meson.build | 36 + .../src/model.rs | 65 +- creds-lib/src/server.rs | 435 ++ creds-ui/Cargo.lock | 3692 ++++++++++++++--- creds-ui/Cargo.toml | 7 +- creds-ui/meson.build | 2 +- creds-ui/src/dbus.rs | 37 + creds-ui/src/gui/mod.rs | 35 +- creds-ui/src/gui/view_model/mod.rs | 25 +- creds-ui/src/main.rs | 136 +- creds-ui/src/meson.build | 6 +- meson.build | 1 + .../Cargo.lock | 290 +- .../Cargo.toml | 3 +- .../src/credential_service/hybrid.rs | 17 +- .../src/credential_service/mod.rs | 32 +- .../src/credential_service/server.rs | 38 +- .../src/credential_service/usb.rs | 32 +- .../src/dbus.rs | 196 +- .../src/dbus/model.rs | 1117 ++--- .../src/main.rs | 2 +- .../src/webauthn.rs | 11 +- 28 files changed, 8222 insertions(+), 1583 deletions(-) create mode 100644 creds-lib/Cargo.lock create mode 100644 creds-lib/Cargo.toml create mode 100644 creds-lib/meson.build create mode 100644 creds-lib/src/client.rs create mode 100644 creds-lib/src/lib.rs create mode 100644 creds-lib/src/meson.build rename {xyz-iinuwa-credential-manager-portal-gtk => creds-lib}/src/model.rs (78%) create mode 100644 creds-lib/src/server.rs create mode 100644 creds-ui/src/dbus.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d7c7926..8a9c2f98 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "type": "lldb", "request": "launch", "name": "Debug executable 'xicm-portal-gtk'", - "program": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/src/xyz-iinuwa-credential-manager-portal-gtk", + "program": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/debug/xyz-iinuwa-credential-manager-portal-gtk", "args": [], "env": { "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/data", diff --git a/creds-lib/Cargo.lock b/creds-lib/Cargo.lock new file mode 100644 index 00000000..1b2c0e44 --- /dev/null +++ b/creds-lib/Cargo.lock @@ -0,0 +1,3523 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.8", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.8", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-url" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2b6c78c06f7288d5e3c3d683bde35a79531127c83b087e5d0d77c974b4b28" +dependencies = [ + "base64", +] + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bluez-async" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" +dependencies = [ + "bitflags 2.9.1", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools 0.14.0", + "log", + "serde", + "serde-xml-rs", + "thiserror 2.0.12", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", +] + +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags 2.9.1", + "bluez-async", + "dashmap 6.1.0", + "dbus", + "futures", + "jni", + "jni-utils", + "log", + "objc2", + "objc2-core-bluetooth", + "objc2-foundation", + "once_cell", + "static_assertions", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "uuid", + "windows", + "windows-future", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbor-smol" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087b31faa4ad4ba21c9bd0209204eef424dae6424195aafc7242006b69fc8d" +dependencies = [ + "delog", + "heapless", + "heapless-bytes", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cosey" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75494895fa1a9713ca725ddf2db084ee84fb0c20938fdd7c89293febe732d30a" +dependencies = [ + "heapless-bytes", + "serde", + "serde_repr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "creds-lib" +version = "0.1.0" +dependencies = [ + "futures-lite", + "libwebauthn", + "serde", + "zbus", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctap-types" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb8b105c5e728afd373e99874f0c1911c170b3a56848456cc16feb4506321606" +dependencies = [ + "bitflags 1.3.2", + "cbor-smol", + "cosey", + "delog", + "heapless", + "heapless-bytes", + "iso7816", + "serde", + "serde-indexed 0.1.1", + "serde_bytes", + "serde_repr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + +[[package]] +name = "delog" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2b93368262340c9d4441251b824500d1b641a50957ecf4219a2cc41b9eac8f" +dependencies = [ + "log", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless-bytes" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7285eba272c6af3e9f15fb9e1c1b6e7d35aa70580ffe0d47af017e97dfb6f48b" +dependencies = [ + "heapless", + "serde", + "typenum", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hidapi" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "windows-sys 0.48.0", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "iso7816" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +dependencies = [ + "heapless", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni", + "log", + "once_cell", + "static_assertions", + "uuid", +] + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "libwebauthn" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ed4eb5e9a63098f7eeaf0fe0fc7b63c84977e8b8253ebe3707b124b6d8036" +dependencies = [ + "aes", + "async-trait", + "base64-url", + "bitflags 2.9.1", + "btleplug", + "byteorder", + "cbc", + "cosey", + "ctap-types", + "curve25519-dalek", + "dbus", + "futures", + "heapless", + "hex", + "hidapi", + "hkdf", + "hmac", + "maplit", + "mockall", + "num-derive", + "num-traits", + "num_enum", + "p256", + "rand 0.8.5", + "rustls", + "serde", + "serde-indexed 0.2.0", + "serde_bytes", + "serde_cbor_2", + "serde_derive", + "serde_repr", + "sha2", + "snow", + "text_io", + "thiserror 2.0.12", + "time", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "tungstenite", + "uuid", + "x509-parser", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags 2.9.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "serdect", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-indexed" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca2da10b1f1623f47130256065e05e94fd7a98dbd26a780a4c5de831b21e5c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde-indexed" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68cf7478db8b81abcf71b6d195a34a4891bd3d39868731c4d73194d74ec7a3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor_2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snow" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "getrandom 0.3.3", + "p256", + "ring", + "rustc_version", + "sha2", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.59.0", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "text_io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d8d3ca3b06292094e03841d8995e910712d2a10b5869c8f9725385b29761115" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + +[[package]] +name = "zbus" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn", + "winnow", +] diff --git a/creds-lib/Cargo.toml b/creds-lib/Cargo.toml new file mode 100644 index 00000000..1f435551 --- /dev/null +++ b/creds-lib/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "creds-lib" +version = "0.1.0" +edition = "2024" + +[dependencies] +futures-lite = "2.6.0" +libwebauthn = "0.2" +serde = { version = "1", features = ["derive"] } +zbus = "5.9.0" diff --git a/creds-lib/meson.build b/creds-lib/meson.build new file mode 100644 index 00000000..6eda8812 --- /dev/null +++ b/creds-lib/meson.build @@ -0,0 +1,27 @@ +common_lib_name = 'xyzii-credman-portal-gtk' +base_id = 'xyz.iinuwa.CredentialManagerUi' + +cargo = find_program('cargo', required: true) + +version = meson.project_version() + +if get_option('profile') == 'development' + profile = 'Devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() + if vcs_tag == '' + version_suffix = '-devel' + else + version_suffix = '-@0@'.format(vcs_tag) + endif + application_id = '@0@.@1@'.format(base_id, profile) +else + profile = '' + version_suffix = '' + application_id = base_id +endif + +meson.add_dist_script( + meson.project_source_root() / 'build-aux/dist-vendor.sh', + meson.project_build_root() / 'meson-dist' / common_lib_name + '-' + version, + meson.project_source_root(), +) diff --git a/creds-lib/src/client.rs b/creds-lib/src/client.rs new file mode 100644 index 00000000..be656fa9 --- /dev/null +++ b/creds-lib/src/client.rs @@ -0,0 +1,25 @@ +use std::pin::Pin; + +use futures_lite::Stream; + +use crate::model::{BackgroundEvent, Device}; + +/// Used for communication from trusted UI to credential service +pub trait CredentialServiceClient { + fn get_available_public_key_devices( + &self, + ) -> impl Future, ()>> + Send; + + fn get_hybrid_credential(&mut self) -> impl Future> + Send; + fn get_usb_credential(&mut self) -> impl Future> + Send; + fn initiate_event_stream( + &mut self, + ) -> impl Future< + Output = Result + Send + 'static>>, ()>, + > + Send; + fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; + fn select_credential( + &self, + credential_id: String, + ) -> impl Future> + Send; +} diff --git a/creds-lib/src/lib.rs b/creds-lib/src/lib.rs new file mode 100644 index 00000000..cd7f939f --- /dev/null +++ b/creds-lib/src/lib.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod model; +pub mod server; diff --git a/creds-lib/src/meson.build b/creds-lib/src/meson.build new file mode 100644 index 00000000..45544aa0 --- /dev/null +++ b/creds-lib/src/meson.build @@ -0,0 +1,36 @@ +cargo_options = [ + '--manifest-path', meson.project_source_root() / meson.current_source_dir() / '..' / 'Cargo.toml', +] +cargo_options += ['--target-dir', meson.project_build_root() / meson.current_build_dir()] + +if get_option('profile') == 'default' + cargo_options += ['--release'] + rust_target = 'release' + message('Building in release mode') +else + rust_target = 'debug' + message('Building in debug mode') +endif + +cargo_env = ['CARGO_HOME=' + meson.project_build_root() / 'cargo-home'] + +custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: common_lib_name, + console: true, + install: true, + install_dir: bindir, + command: [ + 'env', + cargo_env, + cargo, + 'build', + cargo_options, + '&&', + 'cp', + common_lib_name / 'src' / rust_target / common_lib_name, + '@OUTPUT@', + ], +) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs b/creds-lib/src/model.rs similarity index 78% rename from xyz-iinuwa-credential-manager-portal-gtk/src/model.rs rename to creds-lib/src/model.rs index 3a0698ff..f6900ed6 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/model.rs +++ b/creds-lib/src/model.rs @@ -1,37 +1,31 @@ use serde::{Deserialize, Serialize}; -use tokio::sync::oneshot; use zbus::zvariant::{SerializeDict, Type}; -use crate::webauthn::{ +pub use libwebauthn::ops::webauthn::{ Assertion, GetAssertionRequest, MakeCredentialRequest, MakeCredentialResponse, }; -pub struct ViewRequest { - pub operation: Operation, - pub signal: oneshot::Sender<()>, -} - #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Credential { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) username: Option, + pub id: String, + pub name: String, + pub username: Option, } #[derive(Clone, Debug)] -pub(crate) enum CredentialRequest { +pub enum CredentialRequest { CreatePublicKeyCredentialRequest(MakeCredentialRequest), GetPublicKeyCredentialRequest(GetAssertionRequest), } #[derive(Clone, Debug)] -pub(crate) enum CredentialResponse { +pub enum CredentialResponse { CreatePublicKeyCredentialResponse(MakeCredentialResponseInternal), GetPublicKeyCredentialResponse(GetAssertionResponseInternal), } impl CredentialResponse { - pub(crate) fn from_make_credential( + pub fn from_make_credential( response: &MakeCredentialResponse, transports: &[&str], modality: &str, @@ -43,7 +37,7 @@ impl CredentialResponse { )) } - pub(crate) fn from_get_assertion(assertion: &Assertion, modality: &str) -> CredentialResponse { + pub fn from_get_assertion(assertion: &Assertion, modality: &str) -> CredentialResponse { CredentialResponse::GetPublicKeyCredentialResponse(GetAssertionResponseInternal::new( assertion.clone(), modality.to_string(), @@ -52,14 +46,14 @@ impl CredentialResponse { } #[derive(Clone, Debug)] -pub(crate) struct MakeCredentialResponseInternal { - pub(crate) ctap: MakeCredentialResponse, - pub(crate) transport: Vec, - pub(crate) attachment_modality: String, +pub struct MakeCredentialResponseInternal { + pub ctap: MakeCredentialResponse, + pub transport: Vec, + pub attachment_modality: String, } impl MakeCredentialResponseInternal { - pub(crate) fn new( + pub fn new( response: MakeCredentialResponse, transport: Vec, attachment_modality: String, @@ -73,13 +67,13 @@ impl MakeCredentialResponseInternal { } #[derive(Clone, Debug)] -pub(crate) struct GetAssertionResponseInternal { - pub(crate) ctap: Assertion, - pub(crate) attachment_modality: String, +pub struct GetAssertionResponseInternal { + pub ctap: Assertion, + pub attachment_modality: String, } impl GetAssertionResponseInternal { - pub(crate) fn new(ctap: Assertion, attachment_modality: String) -> Self { + pub fn new(ctap: Assertion, attachment_modality: String) -> Self { Self { ctap, attachment_modality, @@ -101,7 +95,7 @@ pub struct GetClientCapabilitiesResponse { pub signal_unknown_credential: bool, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub enum CredentialType { Passkey, // Password, @@ -113,10 +107,10 @@ pub struct Device { pub transport: Transport, } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, Type)] pub enum Operation { - Create { cred_type: CredentialType }, - Get { cred_types: Vec }, + Create, + Get, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -219,23 +213,6 @@ pub enum HybridState { Failed, } -impl From for HybridState { - fn from(value: crate::credential_service::hybrid::HybridState) -> Self { - match value { - crate::credential_service::hybrid::HybridState::Init(qr_code) => { - 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 - } - crate::credential_service::hybrid::HybridState::Failed => HybridState::Failed, - } - } -} - /// Used to share public state between credential service and UI. #[derive(Clone, Debug, Default)] pub enum UsbState { diff --git a/creds-lib/src/server.rs b/creds-lib/src/server.rs new file mode 100644 index 00000000..e79038ba --- /dev/null +++ b/creds-lib/src/server.rs @@ -0,0 +1,435 @@ +use serde::{Deserialize, Serialize}; +use zbus::{ + fdo, + object_server::SignalEmitter, + proxy, + zvariant::{self, DeserializeDict, LE, OwnedValue, SerializeDict, Type, Value}, +}; + +use crate::model::{Operation, ViewUpdate}; + +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub enum BackgroundEvent { + UsbStateChanged(OwnedValue), + HybridStateChanged(OwnedValue), +} + +impl TryFrom for crate::model::BackgroundEvent { + type Error = zvariant::Error; + + fn try_from(value: BackgroundEvent) -> Result { + let ret = match value { + BackgroundEvent::HybridStateChanged(hybrid_state_val) => { + HybridState::try_from(Value::<'_>::from(hybrid_state_val)) + .and_then(crate::model::HybridState::try_from) + .map(crate::model::BackgroundEvent::HybridQrStateChanged) + } + BackgroundEvent::UsbStateChanged(usb_state_val) => { + UsbState::try_from(Value::<'_>::from(usb_state_val)) + .and_then(crate::model::UsbState::try_from) + .map(crate::model::BackgroundEvent::UsbStateChanged) + } + }?; + Ok(ret) + } +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreateCredentialRequest { + pub origin: Option, + pub is_same_origin: Option, + #[zvariant(rename = "type")] + pub r#type: String, + #[zvariant(rename = "publicKey")] + pub public_key: Option, +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreateCredentialResponse { + #[zvariant(rename = "type")] + r#type: String, + public_key: Option, +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreatePublicKeyCredentialRequest { + pub request_json: String, +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct CreatePublicKeyCredentialResponse { + pub registration_response_json: String, +} + +impl From for CreateCredentialResponse { + fn from(response: CreatePublicKeyCredentialResponse) -> Self { + CreateCredentialResponse { + // TODO: Decide on camelCase or kebab-case for cred types + r#type: "public-key".to_string(), + public_key: Some(response), + } + } +} + +#[derive(Serialize, Deserialize, Type)] +pub struct ViewRequest { + pub operation: Operation, +} + +/// Updates to send to the client +#[derive(Serialize, Deserialize, Type)] +pub enum ClientUpdate { + SetTitle(OwnedValue), + SetDevices(OwnedValue), + SetCredentials(OwnedValue), + + WaitingForDevice(OwnedValue), + SelectingDevice(OwnedValue), + + UsbNeedsPin(OwnedValue), + UsbNeedsUserVerification(OwnedValue), + UsbNeedsUserPresence(OwnedValue), + + HybridNeedsQrCode(OwnedValue), + HybridConnecting(OwnedValue), + HybridConnected(OwnedValue), + + Completed(OwnedValue), + Failed(OwnedValue), +} + +impl TryFrom for ViewUpdate { + type Error = zvariant::Error; + fn try_from(value: ClientUpdate) -> std::result::Result { + match value { + ClientUpdate::SetTitle(v) => v.try_into().map(Self::SetTitle), + ClientUpdate::SetDevices(v) => { + let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; + let devices: std::result::Result, zvariant::Error> = + dbus_devices + .into_iter() + .map(|d| { + d.try_into().map_err(|_| { + zvariant::Error::Message( + "Could not deserialize devices".to_string(), + ) + }) + }) + .collect(); + Ok(Self::SetDevices(devices?)) + } + ClientUpdate::SetCredentials(v) => { + let dbus_credentials: Vec = Value::<'_>::from(v).try_into()?; + let credentials: std::result::Result< + Vec, + zvariant::Error, + > = dbus_credentials + .into_iter() + .map(|creds| Ok(creds.into())) + .collect(); + Ok(Self::SetCredentials(credentials?)) + } + + ClientUpdate::WaitingForDevice(v) => { + let dbus_device: Device = Value::<'_>::from(v).try_into()?; + let device: crate::model::Device = dbus_device.try_into().map_err(|_| { + zvariant::Error::Message("Could not deserialize device".to_string()) + })?; + Ok(Self::WaitingForDevice(device)) + } + ClientUpdate::SelectingDevice(_) => Ok(Self::SelectingDevice), + + ClientUpdate::UsbNeedsPin(v) => v.try_into().map(|x: i32| { + let attempts_left = if x == -1 { None } else { Some(x as u32) }; + Self::UsbNeedsPin { attempts_left } + }), + ClientUpdate::UsbNeedsUserVerification(v) => v.try_into().map(|x: i32| { + let attempts_left = if x == -1 { None } else { Some(x as u32) }; + Self::UsbNeedsUserVerification { attempts_left } + }), + ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), + + ClientUpdate::HybridNeedsQrCode(v) => v.try_into().map(Self::HybridNeedsQrCode), + ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), + ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), + + ClientUpdate::Completed(_) => Ok(Self::Completed), + ClientUpdate::Failed(v) => v.try_into().map(Self::Failed), + } + } +} + +#[derive(SerializeDict, DeserializeDict, Type)] +pub struct Credential { + id: String, + name: String, + username: String, +} + +impl From for crate::model::Credential { + fn from(value: Credential) -> Self { + Self { + id: value.id, + name: value.name, + username: if value.username.is_empty() { + None + } else { + Some(value.username) + }, + } + } +} + +impl TryFrom> for Credential { + type Error = zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zvariant::to_bytes(ctx, &value)?; + let credential: Credential = encoded.deserialize()?.0; + Ok(credential) + } +} + +#[derive(SerializeDict, DeserializeDict, Type)] +pub struct Device { + id: String, + transport: String, +} + +impl TryFrom> for Device { + type Error = zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zvariant::to_bytes(ctx, &value)?; + let device: Device = encoded.deserialize()?.0; + Ok(device) + } +} + +impl TryFrom for crate::model::Device { + type Error = (); + fn try_from(value: Device) -> std::result::Result { + let transport = value.transport.try_into().map_err(|_| ())?; + Ok(Self { + id: value.id, + transport, + }) + } +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetCredentialRequest { + pub origin: Option, + pub is_same_origin: Option, + #[zvariant(rename = "type")] + pub r#type: String, + #[zvariant(rename = "publicKey")] + pub public_key: Option, +} + +#[derive(Clone, Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetPublicKeyCredentialRequest { + pub request_json: String, +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetCredentialResponse { + #[zvariant(rename = "type")] + r#type: String, + public_key: Option, +} + +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetPublicKeyCredentialResponse { + pub authentication_response_json: String, +} + +impl From for GetCredentialResponse { + fn from(response: GetPublicKeyCredentialResponse) -> Self { + GetCredentialResponse { + // TODO: Decide on camelCase or kebab-case for cred types + r#type: "public-key".to_string(), + public_key: Some(response), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub enum HybridState { + /// Default state, not listening for hybrid transport. + Idle(OwnedValue), + + /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. + Started(OwnedValue), + + /// BLE advert received, connecting to caBLE tunnel with shared secret. + Connecting(OwnedValue), + + /// Connected to device via caBLE tunnel. + Connected(OwnedValue), + + /// Credential received over tunnel. + Completed(OwnedValue), + + // This isn't actually sent from the server. + UserCancelled(OwnedValue), + + /// Failed to receive a credential + Failed(OwnedValue), +} + +impl TryFrom for crate::model::HybridState { + type Error = zvariant::Error; + fn try_from(value: HybridState) -> std::result::Result { + match value { + HybridState::Idle(_) => Ok(Self::Idle), + HybridState::Started(value) => value.try_into().map(Self::Started), + HybridState::Connecting(_) => Ok(Self::Connecting), + HybridState::Connected(_) => Ok(Self::Connected), + HybridState::Completed(_) => Ok(Self::Completed), + HybridState::UserCancelled(_) => Ok(Self::UserCancelled), + HybridState::Failed(_) => Ok(Self::Failed), + } + } +} + +impl TryFrom> for HybridState { + type Error = zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} + +#[derive(Serialize, Deserialize, Type)] +pub enum ServiceError { + /// Some unknown error with the authenticator occurred. + AuthenticatorError, + + /// No matching credentials were found on the device. + NoCredentials, + + /// Too many incorrect PIN attempts, and authenticator must be removed and + /// reinserted to continue any more PIN attempts. + /// + /// Note that this is different than exhausting the PIN count that fully + /// locks out the device. + PinAttemptsExhausted, + + // TODO: We may want to hide the details on this variant from the public API. + /// Something went wrong with the credential service itself, not the authenticator. + Internal, +} + +impl TryFrom> for ServiceError { + type Error = zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} + +impl From for crate::model::Error { + fn from(value: ServiceError) -> Self { + match value { + ServiceError::AuthenticatorError => Self::AuthenticatorError, + ServiceError::NoCredentials => Self::NoCredentials, + ServiceError::PinAttemptsExhausted => Self::PinAttemptsExhausted, + // TODO: this is bogus, we should refactor to remove the tuple field + // and let the client decide how to render the error. + ServiceError::Internal => { + Self::Internal("Something went wrong. Please try again later.".to_string()) + } + } + } +} + +/// Used to de-/serialize state D-Bus and model::UsbState. +#[derive(Serialize, Deserialize, Type)] +pub enum UsbState { + Idle(OwnedValue), + Waiting(OwnedValue), + SelectingDevice(OwnedValue), + Connected(OwnedValue), + NeedsPin(OwnedValue), /* { + attempts_left: Option, + }, + */ + NeedsUserVerification(OwnedValue), /* { + attempts_left: Option, + },*/ + + NeedsUserPresence(OwnedValue), + //UserCancelled, + SelectCredential(OwnedValue), /* { + creds: Vec, + },*/ + Completed(OwnedValue), + // Failed(crate::credential_service::Error), + Failed(OwnedValue), +} + +impl TryFrom for crate::model::UsbState { + type Error = zvariant::Error; + fn try_from(value: UsbState) -> std::result::Result { + let ret = match value { + UsbState::Idle(_) => Ok(Self::Idle), + UsbState::Waiting(_) => Ok(Self::Waiting), + UsbState::SelectingDevice(_) => Ok(Self::SelectingDevice), + UsbState::Connected(_) => Ok(Self::Connected), + UsbState::NeedsPin(value) => value.try_into().map(|attempts_left: i32| { + let attempts_left = if attempts_left < 0 { + None + } else { + Some(u32::try_from(attempts_left).unwrap()) + }; + Self::NeedsPin { attempts_left } + }), + UsbState::NeedsUserVerification(value) => value.try_into().map(|attempts_left: i32| { + let attempts_left = if attempts_left < 0 { + None + } else { + Some(u32::try_from(attempts_left).unwrap()) + }; + Self::NeedsUserVerification { attempts_left } + }), + UsbState::NeedsUserPresence(_) => Ok(Self::NeedsUserPresence), + UsbState::SelectCredential(value) => value + .try_into() + .map(|creds: Vec| { + creds + .into_iter() + .map(crate::model::Credential::from) + .collect() + }) + .map(|creds| Self::SelectCredential { creds }), + UsbState::Completed(_) => Ok(Self::Completed), + UsbState::Failed(value) => { + ServiceError::try_from(Value::<'_>::from(value)).map(|err| Self::Failed(err.into())) + } + }?; + Ok(ret) + } +} + +impl TryFrom> for UsbState { + type Error = zvariant::Error; + fn try_from(value: Value<'_>) -> std::result::Result { + let ctx = zvariant::serialized::Context::new_dbus(LE, 0); + let encoded = zvariant::to_bytes(ctx, &value)?; + let obj: Self = encoded.deserialize()?.0; + Ok(obj) + } +} diff --git a/creds-ui/Cargo.lock b/creds-ui/Cargo.lock index 6ae8fae7..8d2a7d47 100644 --- a/creds-ui/Cargo.lock +++ b/creds-ui/Cargo.lock @@ -2,6 +2,56 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,6 +61,63 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -76,7 +183,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.0.8", "slab", "windows-sys 0.60.2", ] @@ -92,6 +199,53 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-process" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite", + "rustix 1.0.8", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.60.2", +] + [[package]] name = "async-std" version = "1.13.1" @@ -102,6 +256,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", + "async-process", "crossbeam-utils", "futures-channel", "futures-core", @@ -124,6 +279,26 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -136,18 +311,148 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-url" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2b6c78c06f7288d5e3c3d683bde35a79531127c83b087e5d0d77c974b4b28" +dependencies = [ + "base64", +] + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "blocking" version = "1.6.2" @@ -161,6 +466,63 @@ dependencies = [ "piper", ] +[[package]] +name = "bluez-async" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" +dependencies = [ + "bitflags 2.9.1", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools 0.14.0", + "log", + "serde", + "serde-xml-rs", + "thiserror 2.0.12", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", +] + +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags 2.9.1", + "bluez-async", + "dashmap 6.1.0", + "dbus", + "futures", + "jni", + "jni-utils", + "log", + "objc2", + "objc2-core-bluetooth", + "objc2-foundation", + "once_cell", + "static_assertions", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "uuid", + "windows", + "windows-future", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -173,19 +535,31 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cairo-rs" version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" dependencies = [ - "bitflags", + "bitflags 2.9.1", "cairo-sys-rs", "glib", "libc", @@ -202,15 +576,53 @@ dependencies = [ "system-deps", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbor-smol" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087b31faa4ad4ba21c9bd0209204eef424dae6424195aafc7242006b69fc8d" +dependencies = [ + "delog", + "heapless", + "heapless-bytes", + "serde", +] + [[package]] name = "cc" version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-expr" version = "0.20.1" @@ -228,141 +640,240 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "chacha20" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] [[package]] -name = "demo-ui" -version = "0.1.0" +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "async-std", - "gettext-rs", - "gtk4", - "qrcode", - "serde", - "tracing", + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] [[package]] -name = "errno" -version = "0.3.13" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ + "glob", "libc", - "windows-sys 0.60.2", + "libloading", ] [[package]] -name = "event-listener" -version = "2.5.3" +name = "cmake" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] [[package]] -name = "event-listener" -version = "5.4.0" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "bytes", + "memchr", ] [[package]] -name = "event-listener-strategy" -version = "0.5.4" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "event-listener 5.4.0", - "pin-project-lite", + "crossbeam-utils", ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "field-offset" -version = "0.3.6" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ - "memoffset", - "rustc_version", + "core-foundation-sys", + "libc", ] [[package]] -name = "futures-channel" -version = "0.3.31" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cosey" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75494895fa1a9713ca725ddf2db084ee84fb0c20938fdd7c89293febe732d30a" dependencies = [ - "futures-core", + "heapless-bytes", + "serde", + "serde_repr", ] [[package]] -name = "futures-core" -version = "0.3.31" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] -name = "futures-executor" -version = "0.3.31" +name = "creds-lib" +version = "0.1.0" +dependencies = [ + "futures-lite", + "libwebauthn", + "serde", + "zbus", +] + +[[package]] +name = "creds-ui" +version = "0.1.0" +dependencies = [ + "async-std", + "creds-lib", + "futures-lite", + "gettext-rs", + "gtk4", + "qrcode", + "serde", + "tracing", + "zbus", +] + +[[package]] +name = "critical-section" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", ] [[package]] -name = "futures-io" -version = "0.3.31" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] [[package]] -name = "futures-lite" -version = "2.6.0" +name = "ctap-types" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "bb8b105c5e728afd373e99874f0c1911c170b3a56848456cc16feb4506321606" dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", + "bitflags 1.3.2", + "cbor-smol", + "cosey", + "delog", + "heapless", + "heapless-bytes", + "iso7816", + "serde", + "serde-indexed 0.1.1", + "serde_bytes", + "serde_repr", ] [[package]] -name = "futures-macro" -version = "0.3.31" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", @@ -370,657 +881,2322 @@ dependencies = [ ] [[package]] -name = "futures-task" -version = "0.3.31" +name = "dashmap" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] [[package]] -name = "futures-util" -version = "0.3.31" +name = "dashmap" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "pin-utils", - "slab", + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "gdk-pixbuf" -version = "0.20.10" +name = "data-encoding" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", + "futures-channel", + "futures-util", "libc", + "libdbus-sys", + "winapi", ] [[package]] -name = "gdk-pixbuf-sys" -version = "0.20.10" +name = "dbus-tokio" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "dbus", "libc", - "system-deps", + "tokio", ] [[package]] -name = "gdk4" -version = "0.9.6" +name = "delog" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +checksum = "af2b93368262340c9d4441251b824500d1b641a50957ecf4219a2cc41b9eac8f" dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk4-sys", - "gio", - "glib", - "libc", - "pango", + "log", ] [[package]] -name = "gdk4-sys" -version = "0.9.6" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "gettext-rs" -version = "0.7.2" +name = "der-parser" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44e92f7dc08430aca7ed55de161253a22276dfd69c5526e5c5e95d1f7cf338a" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "gettext-sys", - "locale_config", + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", ] [[package]] -name = "gettext-sys" -version = "0.22.5" +name = "deranged" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb45773f5b8945f12aecd04558f545964f943dacda1b1155b3d738f5469ef661" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ - "cc", - "temp-dir", + "powerfmt", ] [[package]] -name = "gio" -version = "0.20.12" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "pin-project-lite", - "smallvec", + "block-buffer", + "const-oid", + "crypto-common", + "subtle", ] [[package]] -name = "gio-sys" -version = "0.20.10" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys 0.59.0", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "glib" -version = "0.20.12" +name = "downcast" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", ] [[package]] -name = "glib-macros" -version = "0.20.12" +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gettext-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44e92f7dc08430aca7ed55de161253a22276dfd69c5526e5c5e95d1f7cf338a" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb45773f5b8945f12aecd04558f545964f943dacda1b1155b3d738f5469ef661" +dependencies = [ + "cc", + "temp-dir", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags 2.9.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gsk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless-bytes" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7285eba272c6af3e9f15fb9e1c1b6e7d35aa70580ffe0d47af017e97dfb6f48b" +dependencies = [ + "heapless", + "serde", + "typenum", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hidapi" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "windows-sys 0.48.0", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "iso7816" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +dependencies = [ + "heapless", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni", + "log", + "once_cell", + "static_assertions", + "uuid", +] + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "libwebauthn" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ed4eb5e9a63098f7eeaf0fe0fc7b63c84977e8b8253ebe3707b124b6d8036" +dependencies = [ + "aes", + "async-trait", + "base64-url", + "bitflags 2.9.1", + "btleplug", + "byteorder", + "cbc", + "cosey", + "ctap-types", + "curve25519-dalek", + "dbus", + "futures", + "heapless", + "hex", + "hidapi", + "hkdf", + "hmac", + "maplit", + "mockall", + "num-derive", + "num-traits", + "num_enum", + "p256", + "rand 0.8.5", + "rustls", + "serde", + "serde-indexed 0.2.0", + "serde_bytes", + "serde_cbor_2", + "serde_derive", + "serde_repr", + "sha2", + "snow", + "text_io", + "thiserror 2.0.12", + "time", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "tungstenite", + "uuid", + "x509-parser", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags 2.9.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "serdect", + "sha2", +] + +[[package]] +name = "pango" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", + "hmac", + "subtle", ] [[package]] -name = "glib-sys" -version = "0.20.10" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", "libc", - "system-deps", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "gloo-timers" -version = "0.3.0" +name = "rustc-demangle" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] -name = "gobject-sys" -version = "0.20.10" +name = "rustc-hash" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "graphene-rs" -version = "0.20.10" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "glib", - "graphene-sys", - "libc", + "semver", ] [[package]] -name = "graphene-sys" -version = "0.20.10" +name = "rusticata-macros" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "glib-sys", - "libc", - "pkg-config", - "system-deps", + "nom", ] [[package]] -name = "gsk4" -version = "0.9.6" +name = "rustix" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "cairo-rs", - "gdk4", - "glib", - "graphene-rs", - "gsk4-sys", + "bitflags 2.9.1", + "errno", "libc", - "pango", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] -name = "gsk4-sys" -version = "0.9.6" +name = "rustix" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "cairo-sys-rs", - "gdk4-sys", - "glib-sys", - "gobject-sys", - "graphene-sys", + "bitflags 2.9.1", + "errno", "libc", - "pango-sys", - "system-deps", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", ] [[package]] -name = "gtk4" -version = "0.9.7" +name = "rustls" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "cairo-rs", - "field-offset", - "futures-channel", - "gdk-pixbuf", - "gdk4", - "gio", - "glib", - "graphene-rs", - "gsk4", - "gtk4-macros", - "gtk4-sys", - "libc", - "pango", + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "gtk4-macros" -version = "0.9.5" +name = "rustls-native-certs" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", ] [[package]] -name = "gtk4-sys" -version = "0.9.6" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "graphene-sys", - "gsk4-sys", - "libc", - "pango-sys", - "system-deps", + "zeroize", ] [[package]] -name = "hashbrown" -version = "0.15.4" +name = "rustls-webpki" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] [[package]] -name = "heck" -version = "0.5.0" +name = "rustversion" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] -name = "hermit-abi" -version = "0.5.2" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] -name = "image" -version = "0.25.6" +name = "schannel" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "bytemuck", - "byteorder-lite", - "num-traits", + "windows-sys 0.59.0", ] [[package]] -name = "indexmap" -version = "2.10.0" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "equivalent", - "hashbrown", + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", ] [[package]] -name = "js-sys" -version = "0.3.77" +name = "security-framework" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "once_cell", - "wasm-bindgen", + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "kv-log-macro" -version = "1.0.7" +name = "security-framework-sys" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ - "log", + "core-foundation-sys", + "libc", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "semver" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] -name = "libc" -version = "0.2.174" +name = "serde" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] [[package]] -name = "linux-raw-sys" -version = "0.9.4" +name = "serde-indexed" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "fca2da10b1f1623f47130256065e05e94fd7a98dbd26a780a4c5de831b21e5c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "locale_config" -version = "0.3.0" +name = "serde-indexed" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +checksum = "8f68cf7478db8b81abcf71b6d195a34a4891bd3d39868731c4d73194d74ec7a3" dependencies = [ - "lazy_static", - "objc", - "objc-foundation", - "regex", - "winapi", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "log" -version = "0.4.27" +name = "serde-xml-rs" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" dependencies = [ - "value-bag", + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", ] [[package]] -name = "malloc_buf" -version = "0.0.6" +name = "serde_bytes" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" dependencies = [ - "libc", + "serde", ] [[package]] -name = "memchr" -version = "2.7.5" +name = "serde_cbor_2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c" +dependencies = [ + "half", + "serde", +] [[package]] -name = "memoffset" -version = "0.9.1" +name = "serde_derive" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ - "autocfg", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "serde_repr" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ - "autocfg", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "objc" -version = "0.2.7" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "malloc_buf", + "serde", ] [[package]] -name = "objc-foundation" -version = "0.1.1" +name = "serdect" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "block", - "objc", - "objc_id", + "base16ct", + "serde", ] [[package]] -name = "objc_id" -version = "0.1.1" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "objc", + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "pango" -version = "0.20.12" +name = "signal-hook-registry" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ - "gio", - "glib", "libc", - "pango-sys", ] [[package]] -name = "pango-sys" -version = "0.20.10" +name = "signature" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "digest", + "rand_core 0.6.4", ] [[package]] -name = "parking" -version = "2.2.1" +name = "slab" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "snow" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "getrandom 0.3.3", + "p256", + "ring", + "rustc_version", + "sha2", + "subtle", +] [[package]] -name = "piper" -version = "0.2.4" +name = "socket2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", + "libc", + "windows-sys 0.59.0", ] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] -name = "polling" -version = "3.9.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.60.2", + "base64ct", + "der", ] [[package]] -name = "proc-macro-crate" -version = "3.3.0" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "proc-macro2" -version = "1.0.95" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "qrcode" -version = "0.14.1" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" -dependencies = [ - "image", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "quote" -version = "1.0.40" +name = "syn" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "regex" -version = "1.11.1" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "regex-automata" -version = "0.4.9" +name = "system-deps" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", ] [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "target-lexicon" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] -name = "rustc_version" -version = "0.4.1" +name = "temp-dir" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964" [[package]] -name = "rustix" -version = "1.0.8" +name = "tempfile" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.60.2", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.59.0", ] [[package]] -name = "rustversion" -version = "1.0.21" +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] -name = "semver" -version = "1.0.26" +name = "text_io" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "4d8d3ca3b06292094e03841d8995e910712d2a10b5869c8f9725385b29761115" [[package]] -name = "serde" -version = "1.0.219" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "serde_derive", + "thiserror-impl 1.0.69", ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "thiserror" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1028,67 +3204,128 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "thiserror-impl" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", "serde", + "time-core", + "time-macros", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "time-core" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] -name = "slab" -version = "0.4.10" +name = "time-macros" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "tokio" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] [[package]] -name = "syn" -version = "2.0.104" +name = "tokio-macros" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] -name = "system-deps" -version = "7.0.5" +name = "tokio-rustls" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", + "rustls", + "tokio", ] [[package]] -name = "target-lexicon" -version = "0.13.2" +name = "tokio-stream" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] [[package]] -name = "temp-dir" -version = "0.1.16" +name = "tokio-tungstenite" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] [[package]] name = "toml" @@ -1136,30 +3373,100 @@ dependencies = [ ] [[package]] -name = "tracing-attributes" -version = "0.1.30" +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "proc-macro2", - "quote", - "syn", + "crypto-common", + "subtle", ] [[package]] -name = "tracing-core" -version = "0.1.34" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "utf-8" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] [[package]] name = "value-bag" @@ -1173,6 +3480,37 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1254,6 +3592,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1270,18 +3620,141 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1300,6 +3773,21 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1333,6 +3821,21 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1345,6 +3848,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -1357,6 +3866,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1381,6 +3896,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -1393,6 +3914,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -1405,6 +3932,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -1417,6 +3950,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1437,3 +3976,162 @@ checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + +[[package]] +name = "zbus" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn", + "winnow", +] diff --git a/creds-ui/Cargo.toml b/creds-ui/Cargo.toml index 7745f537..23cb3ace 100644 --- a/creds-ui/Cargo.toml +++ b/creds-ui/Cargo.toml @@ -1,12 +1,15 @@ [package] -name = "demo-ui" +name = "creds-ui" version = "0.1.0" edition = "2024" [dependencies] -async-std = "1.13.1" +async-std = { version = "1.13.1", features = ["unstable"] } +creds-lib = { path = "../creds-lib" } +futures-lite = "2.6.0" gettext-rs = { version = "0.7", features = ["gettext-system"] } gtk = { version = "0.9.6", package = "gtk4", features = ["v4_6"] } qrcode = "0.14.1" serde = { version = "1.0.219", features = ["derive"] } tracing = "0.1.41" +zbus = "5.9.0" diff --git a/creds-ui/meson.build b/creds-ui/meson.build index e9984819..30e4ef75 100644 --- a/creds-ui/meson.build +++ b/creds-ui/meson.build @@ -1,7 +1,7 @@ i18n = import('i18n') gnome = import('gnome') -gui_executable_name = 'xyzii-credman-portal-gtk' +gui_executable_name = 'xyzii-creds-portal-gtk' gui_build_dir = meson.current_build_dir() gui_source_dir = meson.current_source_dir() base_id = 'xyz.iinuwa.CredentialManagerUi' diff --git a/creds-ui/src/dbus.rs b/creds-ui/src/dbus.rs new file mode 100644 index 00000000..34259c53 --- /dev/null +++ b/creds-ui/src/dbus.rs @@ -0,0 +1,37 @@ +use async_std::channel::Sender; +use creds_lib::server::{BackgroundEvent, Device, ViewRequest}; +use zbus::{fdo, interface, proxy}; + +#[proxy( + gen_blocking = false, + default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", + default_service = "xyz.iinuwa.credentials.CredentialManagerInternal" +)] +pub trait InternalService { + async fn initiate_event_stream(&self) -> fdo::Result<()>; + + async fn get_available_public_key_devices(&self) -> fdo::Result>; + + async fn get_hybrid_credential(&self) -> fdo::Result<()>; + + async fn get_usb_credential(&self) -> fdo::Result<()>; + + 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<()>; + + #[zbus(signal)] + async fn state_changed(update: BackgroundEvent) -> zbus::Result<()>; +} + +pub struct UiControlService { + pub request_tx: Sender, + // pub update_tx: Sender, +} + +/// These methods are called by the credential service to control the UI. +#[interface(name = "xyz.iinuwa.credentials.UiControl1")] +impl UiControlService { + fn launch_ui(&self, request: creds_lib::server::ViewRequest) {} + // fn send_state_changed(&self, event: BackgroundEvent) {} +} diff --git a/creds-ui/src/gui/mod.rs b/creds-ui/src/gui/mod.rs index cdcd1698..ffe3643d 100644 --- a/creds-ui/src/gui/mod.rs +++ b/creds-ui/src/gui/mod.rs @@ -1,40 +1,36 @@ pub mod view_model; -use std::sync::Arc; use std::thread; +use std::{sync::Arc, thread::JoinHandle}; use async_std::{channel::Receiver, sync::Mutex as AsyncMutex}; -use tokio::sync::oneshot; -use crate::credential_service::CredentialServiceClient; -use crate::model::{Operation, ViewRequest, ViewUpdate}; +use creds_lib::server::ViewRequest; +use creds_lib::{ + client::CredentialServiceClient, + model::{Operation, ViewUpdate}, +}; use view_model::ViewEvent; pub(super) fn start_gui_thread( rx: Receiver, client: C, -) { - thread::Builder::new() - .name("gui".into()) - .spawn(move || { - let client = Arc::new(AsyncMutex::new(client)); - // 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(); +) -> Result, std::io::Error> { + thread::Builder::new().name("gui".into()).spawn(move || { + let client = Arc::new(AsyncMutex::new(client)); + // D-Bus received a request and needs a window open + while let Ok(view_request) = rx.recv_blocking() { + run_gui(client.clone(), view_request); + } + }) } fn run_gui( client: Arc>, request: ViewRequest, ) { - let ViewRequest { - operation, - signal: response_tx, - } = request; + let operation = request.operation; 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 { @@ -46,7 +42,6 @@ fn run_gui( view_model::gtk::start_gtk_app(tx_event, rx_update); async_std::task::block_on(event_loop.cancel()); - response_tx.send(()).unwrap(); } trait GuiClient { diff --git a/creds-ui/src/gui/view_model/mod.rs b/creds-ui/src/gui/view_model/mod.rs index aa8161f8..7b6499e2 100644 --- a/creds-ui/src/gui/view_model/mod.rs +++ b/creds-ui/src/gui/view_model/mod.rs @@ -8,13 +8,14 @@ use async_std::{ sync::Mutex as AsyncMutex, }; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; use tracing::{error, info}; -use crate::credential_service::CredentialServiceClient; -use crate::model::{ - BackgroundEvent, Credential, Device, Error, HybridState, Operation, Transport, UsbState, - ViewUpdate, +use creds_lib::{ + client::CredentialServiceClient, + model::{ + BackgroundEvent, Credential, Device, Error, HybridState, Operation, Transport, UsbState, + ViewUpdate, + }, }; #[derive(Debug)] @@ -34,7 +35,7 @@ where selected_device: Option, // providers: Vec, - usb_cred_tx: Option>>>, + usb_cred_tx: Option>>>, hybrid_qr_state: HybridState, hybrid_qr_code_data: Option>, @@ -213,9 +214,15 @@ impl ViewModel { // TODO: Provide more specific error messages using the wrapped Error. UsbState::Failed(err) => { let error_msg = String::from(match err { - Error::NoCredentials => "No matching credentials found on this authenticator.", - Error::PinAttemptsExhausted => "No more PIN attempts allowed. Try removing your device and plugging it back in.", - Error::AuthenticatorError | Error::Internal(_) => "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", + Error::NoCredentials => { + "No matching credentials found on this authenticator." + } + Error::PinAttemptsExhausted => { + "No more PIN attempts allowed. Try removing your device and plugging it back in." + } + Error::AuthenticatorError | Error::Internal(_) => { + "Something went wrong while retrieving a credential. Please try again later or use a different authenticator." + } }); self.tx_update .send(ViewUpdate::Failed(error_msg)) diff --git a/creds-ui/src/main.rs b/creds-ui/src/main.rs index 26919ed5..a05493b5 100644 --- a/creds-ui/src/main.rs +++ b/creds-ui/src/main.rs @@ -1,20 +1,140 @@ #[rustfmt::skip] mod config; +mod dbus; mod gui; -use std::sync::Arc; +use std::error::Error; -fn main() { - println!("Hello, world!"); +use async_std::{ + channel::{Receiver, Sender}, + stream::Stream, +}; +use creds_lib::client::CredentialServiceClient; +use futures_lite::StreamExt; +use zbus::{Connection, zvariant}; + +use crate::dbus::{InternalServiceProxy, UiControlService}; + +fn main() -> Result<(), Box> { + async_std::task::block_on(run()) +} + +async fn run() -> Result<(), Box> { print!("Starting GUI thread...\t"); + 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 (dbus_to_gui_tx, dbus_to_gui_rx) = async_std::channel::unbounded(); - gui::start_gui_thread(dbus_to_gui_rx, Arc::new(cred_client)); + let conn = zbus::connection::Builder::session()?.build().await?; + let cred_client = DbusCredentialClient::new(conn); + let handle = gui::start_gui_thread(request_rx, cred_client)?; println!(" ✅"); + handle.join(); + + let interface = UiControlService { request_tx }; + let path = "/xyz/iinuwa/credentials/UiControl"; + let service = "xyz.iinuwa.credentials.UiControl"; + let _server_conn = zbus::connection::Builder::session()? + .name(service)? + .serve_at(path, interface)? + .build() + .await?; + loop { + std::future::pending::<()>().await; + } + #[allow(unreachable_code)] + Ok(()) +} + +pub struct DbusCredentialClient { + conn: Connection, } -trait UiControlService { - fn launch_ui(&self); - fn send_state_changed(&self); +impl DbusCredentialClient { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + async fn proxy(&self) -> std::result::Result { + InternalServiceProxy::new(&self.conn) + .await + .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) + } +} + +impl CredentialServiceClient for DbusCredentialClient { + async fn get_available_public_key_devices( + &self, + ) -> std::result::Result, ()> { + let dbus_devices = self + .proxy() + .await? + .get_available_public_key_devices() + .await + .map_err(|_| ())?; + dbus_devices.into_iter().map(|d| d.try_into()).collect() + } + + async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { + self.proxy() + .await? + .get_hybrid_credential() + .await + .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) + .map_err(|_| ()) + } + + async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { + self.proxy() + .await? + .get_hybrid_credential() + .await + .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) + .map_err(|_| ()) + } + + async fn initiate_event_stream( + &mut self, + ) -> std::result::Result< + std::pin::Pin + Send + 'static>>, + (), + > { + let stream = self + .proxy() + .await? + .receive_state_changed() + .await + .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? + .filter_map(|msg| { + msg.args() + .and_then(|args| { + args.update + .try_into() + .map_err(|err: zvariant::Error| err.into()) + }) + .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) + .ok() + }) + .boxed(); + self.proxy() + .await? + .initiate_event_stream() + .await + .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) + .and_then(|_| Ok(stream)) + } + + async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { + self.proxy() + .await? + .enter_client_pin(pin) + .await + .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) + } + + async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { + self.proxy() + .await? + .select_credential(credential_id) + .await + .map_err(|err| tracing::error!("Failed to select credential: {err}")) + } } diff --git a/creds-ui/src/meson.build b/creds-ui/src/meson.build index db6fb430..bf264612 100644 --- a/creds-ui/src/meson.build +++ b/creds-ui/src/meson.build @@ -1,3 +1,5 @@ +gui_cargo_package_name = 'creds-ui' + global_conf = configuration_data() global_conf.set_quoted('APP_ID', application_id) if (get_option('profile') == 'development') @@ -25,7 +27,7 @@ run_command( cargo_options = [ '--manifest-path', meson.project_source_root() / gui_source_dir / 'Cargo.toml', ] -cargo_options += ['--target-dir', meson.project_build_root() / gui_executable_name / 'src'] +cargo_options += ['--target-dir', meson.project_build_root() / gui_build_dir] if get_option('profile') == 'default' cargo_options += ['--release'] @@ -55,7 +57,7 @@ custom_target( cargo_options, '&&', 'cp', - gui_executable_name / 'src' / rust_target / gui_executable_name, + gui_build_dir / rust_target / gui_cargo_package_name, '@OUTPUT@', ], ) diff --git a/meson.build b/meson.build index 43a99557..4f31c227 100644 --- a/meson.build +++ b/meson.build @@ -20,5 +20,6 @@ meson.add_dist_script( meson.project_source_root(), ) +subdir('creds-lib') subdir('xyz-iinuwa-credential-manager-portal-gtk') subdir('creds-ui') diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock index a4ad99fe..895a954b 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock @@ -118,6 +118,79 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.8", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.8", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -129,6 +202,24 @@ dependencies = [ "syn", ] +[[package]] +name = "async-signal" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.60.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -151,6 +242,12 @@ dependencies = [ "syn", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.88" @@ -171,6 +268,12 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -313,6 +416,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bluez-async" version = "0.8.0" @@ -566,6 +682,16 @@ dependencies = [ "libc", ] +[[package]] +name = "creds-lib" +version = "0.1.0" +dependencies = [ + "futures-lite", + "libwebauthn", + "serde", + "zbus", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1273,6 +1399,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1473,8 +1605,9 @@ dependencies = [ [[package]] name = "libwebauthn" -version = "0.2.1" -source = "git+https://github.com/linux-credentials/libwebauthn?rev=34f8a59cb1634175b8baf866e6d30d1869f5a221#34f8a59cb1634175b8baf866e6d30d1869f5a221" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ed4eb5e9a63098f7eeaf0fe0fc7b63c84977e8b8253ebe3707b124b6d8036" dependencies = [ "aes", "async-trait", @@ -1624,9 +1757,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -1919,6 +2052,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1935,6 +2079,20 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -2218,9 +2376,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags 2.9.0", "errno", @@ -2653,7 +2811,7 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix 1.0.5", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -3132,6 +3290,12 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-result" version = "0.1.2" @@ -3168,6 +3332,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3192,13 +3365,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3211,6 +3401,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3223,6 +3419,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3235,12 +3437,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3253,6 +3467,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3265,6 +3485,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3277,6 +3503,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3289,6 +3521,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.6" @@ -3324,16 +3562,6 @@ dependencies = [ "time", ] -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "xml-rs" version = "0.8.26" @@ -3348,6 +3576,7 @@ dependencies = [ "async-trait", "base64", "cosey", + "creds-lib", "futures-lite", "gio", "libwebauthn", @@ -3364,13 +3593,19 @@ dependencies = [ [[package]] name = "zbus" -version = "5.5.0" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" dependencies = [ "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "enumflags2", "event-listener", "futures-core", @@ -3380,13 +3615,11 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", - "static_assertions", "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", "winnow", - "xdg-home", "zbus_macros", "zbus_names", "zvariant", @@ -3394,9 +3627,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.5.0" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" +checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3447,14 +3680,13 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zvariant" -version = "5.4.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", "winnow", "zvariant_derive", "zvariant_utils", @@ -3462,9 +3694,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.4.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml index d8072200..9167fc9c 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml +++ b/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml @@ -9,6 +9,7 @@ lto = true [dependencies] base64 = "0.22.1" +creds-lib = { path = "../creds-lib" } openssl = "0.10.72" ring = "0.17.14" serde = { version = "1.0.219", features = ["derive"] } @@ -17,7 +18,7 @@ serde_json = "1.0.140" tracing = "0.1.41" tracing-subscriber = "0.3" zbus = { version = "5.5.0", default-features = false, features = ["blocking-api", "tokio"] } -libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "34f8a59cb1634175b8baf866e6d30d1869f5a221" } +libwebauthn = "~0.2.2" async-trait = "0.1.88" tokio = { version = "1.45.0", features = ["rt-multi-thread"] } futures-lite = "2.6.0" 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 a3799799..e239424b 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 @@ -12,7 +12,7 @@ use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOpe use libwebauthn::transport::{Channel, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; -use crate::model::{CredentialRequest, Error}; +use creds_lib::model::{CredentialRequest, Error}; use super::AuthenticatorResponse; @@ -205,6 +205,19 @@ impl From for HybridState { } } +impl From for creds_lib::model::HybridState { + fn from(value: HybridState) -> Self { + match value { + HybridState::Init(qr_code) => creds_lib::model::HybridState::Started(qr_code), + HybridState::Connecting => creds_lib::model::HybridState::Connecting, + HybridState::Connected => creds_lib::model::HybridState::Connected, + HybridState::Completed => creds_lib::model::HybridState::Completed, + HybridState::UserCancelled => creds_lib::model::HybridState::UserCancelled, + HybridState::Failed => creds_lib::model::HybridState::Failed, + } + } +} + async fn handle_hybrid_updates( state_sender: &Sender, mut ux_update_receiver: broadcast::Receiver, @@ -249,7 +262,7 @@ pub(super) mod test { proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2Transport}, }; - use crate::model::CredentialRequest; + use creds_lib::model::CredentialRequest; use super::{HybridEvent, HybridHandler, HybridStateInternal}; #[derive(Debug)] 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 08d84b7b..21bdf3a3 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 @@ -15,15 +15,17 @@ use libwebauthn::{ ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, }; -use crate::{ - credential_service::{hybrid::HybridEvent, usb::UsbEvent}, +use creds_lib::{ + client::CredentialServiceClient, model::{CredentialRequest, CredentialResponse, Device, Transport}, }; +use crate::credential_service::{hybrid::HybridEvent, usb::UsbEvent}; + use hybrid::{HybridHandler, HybridState, HybridStateInternal}; use usb::{UsbHandler, UsbStateInternal}; pub use { - server::{CredentialManagementClient, CredentialServiceClient, InProcessServer}, + server::{CredentialManagementClient, InProcessServer}, usb::UsbState, }; @@ -211,10 +213,10 @@ mod test { use futures_lite::stream::StreamExt; - use crate::{ - credential_service::usb::InProcessUsbHandler, - dbus::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}, + use crate::credential_service::usb::InProcessUsbHandler; + use creds_lib::{ model::CredentialRequest, + server::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}, }; use super::{ @@ -222,6 +224,7 @@ mod test { AuthenticatorResponse, CredentialService, }; + /* #[test] fn test_hybrid_sets_credential() { let request = create_credential_request(); @@ -285,16 +288,19 @@ mod test { "credProps": true } }"#.to_string(); - let (req, _) = CreateCredentialRequest { - origin: Some("webauthn.io".to_string()), - is_same_origin: Some(true), - r#type: "public-key".to_string(), - public_key: Some(CreatePublicKeyCredentialRequest { request_json }), - } - .try_into_ctap2_request() + + let (req, _) = crate::dbus::model::create_credential_request_try_into_ctap2( + &CreateCredentialRequest { + origin: Some("webauthn.io".to_string()), + is_same_origin: Some(true), + r#type: "public-key".to_string(), + public_key: Some(CreatePublicKeyCredentialRequest { request_json }), + }, + ) .unwrap(); CredentialRequest::CreatePublicKeyCredentialRequest(req) } + */ fn create_authenticator_response() -> AuthenticatorResponse { use libwebauthn::{ 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 53bc0599..a371d1b2 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 @@ -3,10 +3,11 @@ use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; +use creds_lib::client::CredentialServiceClient; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; -use crate::model::{BackgroundEvent, CredentialRequest, CredentialResponse, Device}; +use creds_lib::model::{BackgroundEvent, CredentialRequest, CredentialResponse, Device}; use super::hybrid::{HybridHandler, HybridState}; use super::usb::{UsbHandler, UsbState}; @@ -93,26 +94,6 @@ enum InProcessServerResponse { Management(ManagementResponse), } -/// Used for communication from trusted UI to credential service -pub trait CredentialServiceClient { - fn get_available_public_key_devices( - &self, - ) -> impl Future, ()>> + Send; - - fn get_hybrid_credential(&mut self) -> impl Future> + Send; - fn get_usb_credential(&mut self) -> impl Future> + Send; - fn initiate_event_stream( - &mut self, - ) -> impl Future< - Output = Result + Send + 'static>>, ()>, - > + Send; - fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; - fn select_credential( - &self, - credential_id: String, - ) -> impl Future> + Send; -} - /// Used for communication from privileged broker to credential service pub trait CredentialManagementClient { fn init_request( @@ -414,35 +395,36 @@ impl CredentialServiceClient for InProcessClient { } } -impl CredentialServiceClient for Arc { +struct ArcInProcessClient(Arc); +impl CredentialServiceClient for ArcInProcessClient { fn get_available_public_key_devices(&self) -> impl Future, ()>> { - InProcessClient::get_available_public_key_devices(self) + InProcessClient::get_available_public_key_devices(&self.0) } async fn get_hybrid_credential(&mut self) -> Result<(), ()> { - let client = Arc::get_mut(self).ok_or(())?; + let client = Arc::get_mut(&mut self.0).ok_or(())?; InProcessClient::get_hybrid_credential(client).await } async fn get_usb_credential(&mut self) -> Result<(), ()> { - let client = Arc::get_mut(self).ok_or(())?; + let client = Arc::get_mut(&mut self.0).ok_or(())?; InProcessClient::get_usb_credential(client).await } async fn initiate_event_stream( &mut self, ) -> Result + Send + 'static>>, ()> { - let client = Arc::get_mut(self).ok_or(())?; + let client = Arc::get_mut(&mut self.0).ok_or(())?; InProcessClient::initiate_event_stream(client).await } async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { - let client = Arc::get_mut(self).ok_or(())?; + let client = Arc::get_mut(&mut self.0).ok_or(())?; InProcessClient::enter_client_pin(client, pin).await } fn select_credential(&self, credential_id: String) -> impl Future> { - InProcessClient::select_credential(self, credential_id) + InProcessClient::select_credential(&self.0, credential_id) } } 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 ce5ea665..1e5d9496 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 @@ -17,7 +17,7 @@ use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; -use crate::model::{Credential, CredentialRequest, Error, GetAssertionResponseInternal}; +use creds_lib::model::{Credential, CredentialRequest, Error, GetAssertionResponseInternal}; use super::{AuthenticatorResponse, CredentialResponse}; @@ -524,32 +524,34 @@ impl From for UsbState { } } -impl From for crate::model::UsbState { +impl From for creds_lib::model::UsbState { fn from(value: UsbState) -> Self { Self::from(&value) } } -impl From<&UsbState> for crate::model::UsbState { +impl From<&UsbState> for creds_lib::model::UsbState { fn from(value: &UsbState) -> Self { match value { - UsbState::Idle => crate::model::UsbState::Idle, - UsbState::Waiting => crate::model::UsbState::Waiting, - UsbState::SelectingDevice => crate::model::UsbState::SelectingDevice, - UsbState::Connected => crate::model::UsbState::Connected, - UsbState::NeedsPin { attempts_left, .. } => crate::model::UsbState::NeedsPin { + UsbState::Idle => creds_lib::model::UsbState::Idle, + UsbState::Waiting => creds_lib::model::UsbState::Waiting, + UsbState::SelectingDevice => creds_lib::model::UsbState::SelectingDevice, + UsbState::Connected => creds_lib::model::UsbState::Connected, + UsbState::NeedsPin { attempts_left, .. } => creds_lib::model::UsbState::NeedsPin { attempts_left: *attempts_left, }, UsbState::NeedsUserVerification { attempts_left } => { - crate::model::UsbState::NeedsUserVerification { + creds_lib::model::UsbState::NeedsUserVerification { attempts_left: *attempts_left, } } - UsbState::NeedsUserPresence => crate::model::UsbState::NeedsUserPresence, - UsbState::SelectCredential { creds, .. } => crate::model::UsbState::SelectCredential { - creds: creds.to_owned(), - }, - UsbState::Completed => crate::model::UsbState::Completed, - UsbState::Failed(err) => crate::model::UsbState::Failed(err.to_owned()), + UsbState::NeedsUserPresence => creds_lib::model::UsbState::NeedsUserPresence, + UsbState::SelectCredential { creds, .. } => { + creds_lib::model::UsbState::SelectCredential { + creds: creds.to_owned(), + } + } + UsbState::Completed => creds_lib::model::UsbState::Completed, + UsbState::Failed(err) => creds_lib::model::UsbState::Failed(err.to_owned()), } } } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index b3630170..133d101a 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -31,31 +31,40 @@ mod model; +use creds_lib::server::{CreateCredentialRequest, ViewRequest}; use futures_lite::StreamExt; use std::collections::VecDeque; -use std::sync::mpsc::Sender; +use std::error::Error; use std::sync::Arc; use tokio::sync::Mutex as AsyncMutex; use zbus::object_server::SignalEmitter; -use zbus::zvariant; +use zbus::proxy; use zbus::{ connection::{self, Connection}, fdo, interface, Result, }; -use crate::credential_service::{CredentialManagementClient, CredentialServiceClient}; -use crate::model::{ - CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, - Operation, ViewRequest, +use creds_lib::{ + client::CredentialServiceClient, + model::{ + CredentialRequest, CredentialResponse, CredentialType, GetClientCapabilitiesResponse, + Operation, + }, + server::{ + BackgroundEvent, CreateCredentialResponse, CreatePublicKeyCredentialResponse, Device, + GetCredentialRequest, GetCredentialResponse, GetPublicKeyCredentialResponse, + }, }; use self::model::{ - BackgroundEvent, CreateCredentialResponse, CreatePublicKeyCredentialResponse, Device, - GetCredentialRequest, GetCredentialResponse, GetPublicKeyCredentialResponse, + create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, + get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, }; +use crate::credential_service::CredentialManagementClient; + // TODO: This is a workaround for testing credential_service. Refactor so that // these private structs don't need to be exported. -pub use self::model::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}; +// pub use self::model::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}; pub(crate) async fn start_service( service_name: &str, @@ -113,7 +122,7 @@ impl CredentialManager ))); } let (make_cred_request, client_data_json) = - request.clone().try_into_ctap2_request().map_err(|e| { + create_credential_request_try_into_ctap2(&request).map_err(|e| { fdo::Error::Failed(format!( "Could not parse passkey creation request: {e:?}" )) @@ -127,11 +136,10 @@ impl CredentialManager if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { - let public_key_response = - CreatePublicKeyCredentialResponse::try_from_ctap2_response( - &cred_response, - client_data_json, - )?; + let public_key_response = create_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; Ok(public_key_response.into()) } else { Err(fdo::Error::Failed("Failed to create passkey".to_string())) @@ -175,7 +183,7 @@ impl CredentialManager // - query for related origins, if supported // - fail if not supported, or if RP ID doesn't match any related origins. let (get_cred_request, client_data_json) = - request.clone().try_into_ctap2_request().map_err(|_| { + get_credential_request_try_into_ctap2(&request).map_err(|_| { fdo::Error::Failed( "Could not parse passkey assertion request.".to_owned(), ) @@ -188,11 +196,10 @@ impl CredentialManager match response { CredentialResponse::GetPublicKeyCredentialResponse(cred_response) => { - let public_key_response = - GetPublicKeyCredentialResponse::try_from_ctap2_response( - &cred_response, - client_data_json, - )?; + let public_key_response = get_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; Ok(public_key_response.into()) } _ => Err(fdo::Error::Failed( @@ -312,25 +319,35 @@ impl InternalService { ) -> zbus::Result<()>; } -struct UiControlServiceImpl; - /// These methods are called by the credential service to control the UI. -#[interface( - name = "xyz.iinuwa.credentials.UiControl1", - proxy( - gen_blocking = false, - default_path = "/xyz/iinuwa/credentials/UiControl", - default_service = "xyz.iinuwa.credentials.UiControl", - ) +#[proxy( + gen_blocking = false, + default_path = "/xyz/iinuwa/credentials/UiControl", + default_service = "xyz.iinuwa.credentials.UiControl" )] -impl UiControlService for UiControlServiceImpl { - fn launch_ui(&self) {} - fn send_state_changed(&self) {} +trait UiControlServiceClient { + fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; } -trait UiControlService { - fn launch_ui(&self); - fn send_state_changed(&self); +trait UiControlServiceClient { + async fn launch_ui(&self, request: ViewRequest) -> std::result::Result<(), Box>; +} +struct UiControlServiceImpl { + conn: Connection, +} +impl UiControlServiceImpl { + async fn proxy(&self) -> std::result::Result { + UiControlServiceClientProxy::new(&self.conn).await + } +} +impl UiControlServiceClient for UiControlServiceImpl { + async fn launch_ui(&self, request: ViewRequest) -> std::result::Result<(), Box> { + self.proxy() + .await? + .launch_ui(request) + .await + .map_err(|err| err.into()) + } } async fn execute_flow( @@ -346,21 +363,18 @@ async fn execute_flow( // start GUI let operation = match &cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create { - cred_type: CredentialType::Passkey, - }, - CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get { - cred_types: vec![CredentialType::Passkey], - }, + CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, + CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, }; let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); + /* let view_request = ViewRequest { operation, signal: signal_tx, }; // TODO: Replace this with a UiControlClient // 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()) @@ -372,99 +386,3 @@ async fn execute_flow( zbus::Error::Failure("Error retrieving credential".to_string()) }) } - -pub struct DbusCredentialClient { - conn: Connection, -} - -impl DbusCredentialClient { - pub fn new(conn: Connection) -> Self { - Self { conn } - } - async fn proxy(&self) -> std::result::Result { - InternalServiceProxy::new(&self.conn) - .await - .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) - } -} - -impl CredentialServiceClient for DbusCredentialClient { - async fn get_available_public_key_devices( - &self, - ) -> std::result::Result, ()> { - let dbus_devices = self - .proxy() - .await? - .get_available_public_key_devices() - .await - .map_err(|_| ())?; - dbus_devices.into_iter().map(|d| d.try_into()).collect() - } - - async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { - self.proxy() - .await? - .get_hybrid_credential() - .await - .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) - .map_err(|_| ()) - } - - async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { - self.proxy() - .await? - .get_hybrid_credential() - .await - .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) - .map_err(|_| ()) - } - - async fn initiate_event_stream( - &mut self, - ) -> std::result::Result< - std::pin::Pin< - Box + Send + 'static>, - >, - (), - > { - let stream = self - .proxy() - .await? - .receive_state_changed() - .await - .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? - .filter_map(|msg| { - msg.args() - .and_then(|args| { - args.update - .try_into() - .map_err(|err: zvariant::Error| err.into()) - }) - .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) - .ok() - }) - .boxed(); - self.proxy() - .await? - .initiate_event_stream() - .await - .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) - .and_then(|_| Ok(stream)) - } - - async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { - self.proxy() - .await? - .enter_client_pin(pin) - .await - .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) - } - - async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { - self.proxy() - .await? - .select_credential(credential_id) - .await - .map_err(|err| tracing::error!("Failed to select credential: {err}")) - } -} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs index 60062f7b..dae18f6f 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs @@ -1,20 +1,33 @@ //! This module contains types used for serializing data to and from D-Bus method calls. //! -//! Types shared between components within this service belong in crate::model. +//! Types shared between components within this service belong in creds_lib::model. use std::{collections::HashMap, time::Duration}; use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot; use zbus::{ fdo, zvariant::{self, DeserializeDict, OwnedValue, SerializeDict, Type, Value, LE}, }; -use crate::model::{ - CredentialType, GetAssertionResponseInternal, MakeCredentialResponseInternal, Operation, - ViewUpdate, +use creds_lib::{ + model::{ + CredentialType, GetAssertionResponseInternal, MakeCredentialResponseInternal, Operation, + ViewUpdate, + }, + server::{ + CreateCredentialRequest, CreatePublicKeyCredentialResponse, GetCredentialRequest, + GetPublicKeyCredentialResponse, + }, }; + +pub(super) struct ViewRequest { + pub(super) operation: Operation, + pub(super) signal: oneshot::Sender<()>, +} + use crate::webauthn::{ self, CredentialProtectionExtension, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity, @@ -24,71 +37,33 @@ use crate::webauthn::{ PublicKeyCredentialParameters, ResidentKeyRequirement, UserVerificationRequirement, }; -// D-Bus <-> Client types -#[derive(Clone, Debug, Serialize, Deserialize, Type)] -pub(super) enum BackgroundEvent { - UsbStateChanged(OwnedValue), - HybridStateChanged(OwnedValue), -} - -impl TryFrom for crate::model::BackgroundEvent { - type Error = zvariant::Error; - - fn try_from(value: BackgroundEvent) -> Result { - let ret = match value { - BackgroundEvent::HybridStateChanged(hybrid_state_val) => { - HybridState::try_from(Value::<'_>::from(hybrid_state_val)) - .and_then(crate::model::HybridState::try_from) - .map(crate::model::BackgroundEvent::HybridQrStateChanged) - } - BackgroundEvent::UsbStateChanged(usb_state_val) => { - UsbState::try_from(Value::<'_>::from(usb_state_val)) - .and_then(crate::model::UsbState::try_from) - .map(crate::model::BackgroundEvent::UsbStateChanged) - } - }?; - Ok(ret) +// Helper functions for translating D-Bus types into internal types +pub(super) fn create_credential_request_try_into_ctap2( + request: &CreateCredentialRequest, +) -> std::result::Result<(MakeCredentialRequest, String), webauthn::Error> { + if request.public_key.is_none() { + return Err(webauthn::Error::NotSupported); } -} - -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreateCredentialRequest { - pub(crate) origin: Option, - pub(crate) is_same_origin: Option, - #[zvariant(rename = "type")] - pub(crate) r#type: String, - #[zvariant(rename = "publicKey")] - pub(crate) public_key: Option, -} - -impl CreateCredentialRequest { - pub(crate) fn try_into_ctap2_request( - &self, - ) -> std::result::Result<(MakeCredentialRequest, String), webauthn::Error> { - if self.public_key.is_none() { - return Err(webauthn::Error::NotSupported); - } - let options = self.public_key.as_ref().unwrap(); - - let request_value = serde_json::from_str::(&options.request_json) - .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; - let json = request_value - .as_object() - .ok_or_else(|| webauthn::Error::Internal("Invalid request JSON".to_string()))?; - let challenge = json - .get("challenge") - .and_then(|c| c.as_str()) - .ok_or_else(|| webauthn::Error::Internal("JSON missing `challenge` field".to_string()))? - .to_owned(); - let rp = json - .get("rp") - .and_then(|val| { - serde_json::from_str::(&val.to_string()).ok() - }) - .ok_or_else(|| webauthn::Error::Internal("JSON missing `rp` field".to_string()))?; - let user = json - .get("user") + let options = request.public_key.as_ref().unwrap(); + + let request_value = serde_json::from_str::(&options.request_json) + .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let json = request_value + .as_object() + .ok_or_else(|| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let challenge = json + .get("challenge") + .and_then(|c| c.as_str()) + .ok_or_else(|| webauthn::Error::Internal("JSON missing `challenge` field".to_string()))? + .to_owned(); + let rp = json + .get("rp") + .and_then(|val| { + serde_json::from_str::(&val.to_string()).ok() + }) + .ok_or_else(|| webauthn::Error::Internal("JSON missing `rp` field".to_string()))?; + let user = + json.get("user") .ok_or(webauthn::Error::Internal( "JSON missing `user` field".to_string(), )) @@ -99,719 +74,327 @@ impl CreateCredentialRequest { webauthn::Error::Internal(msg) }) })?; - let other_options = - serde_json::from_str::(&request_value.to_string()) - .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; - let (resident_key, user_verification) = - if let Some(authenticator_selection) = other_options.authenticator_selection { - let resident_key = match authenticator_selection.resident_key.as_deref() { - Some("required") => Some(ResidentKeyRequirement::Required), - Some("preferred") => Some(ResidentKeyRequirement::Preferred), - Some("discouraged") => Some(ResidentKeyRequirement::Discouraged), - Some(_) => None, - // legacy webauthn-1 member - None if authenticator_selection.require_resident_key == Some(true) => { - Some(ResidentKeyRequirement::Required) - } - None => None, - }; + let other_options = + serde_json::from_str::(&request_value.to_string()) + .map_err(|_| webauthn::Error::Internal("Invalid request JSON".to_string()))?; + let (resident_key, user_verification) = + if let Some(authenticator_selection) = other_options.authenticator_selection { + let resident_key = match authenticator_selection.resident_key.as_deref() { + Some("required") => Some(ResidentKeyRequirement::Required), + Some("preferred") => Some(ResidentKeyRequirement::Preferred), + Some("discouraged") => Some(ResidentKeyRequirement::Discouraged), + Some(_) => None, + // legacy webauthn-1 member + None if authenticator_selection.require_resident_key == Some(true) => { + Some(ResidentKeyRequirement::Required) + } + None => None, + }; - let user_verification = authenticator_selection - .user_verification - .map(|uv| match uv.as_ref() { - "required" => UserVerificationRequirement::Required, - "preferred" => UserVerificationRequirement::Preferred, - "discouraged" => UserVerificationRequirement::Discouraged, - _ => todo!("This should be fixed in the future"), - }) - .unwrap_or(UserVerificationRequirement::Preferred); + let user_verification = authenticator_selection + .user_verification + .map(|uv| match uv.as_ref() { + "required" => UserVerificationRequirement::Required, + "preferred" => UserVerificationRequirement::Preferred, + "discouraged" => UserVerificationRequirement::Discouraged, + _ => todo!("This should be fixed in the future"), + }) + .unwrap_or(UserVerificationRequirement::Preferred); - (resident_key, user_verification) - } else { - (None, UserVerificationRequirement::Preferred) - }; - let extensions = if let Some(incoming_extensions) = other_options.extensions { - let extensions = MakeCredentialsRequestExtensions { - cred_props: incoming_extensions.cred_props, - cred_blob: incoming_extensions - .cred_blob - .and_then(|x| URL_SAFE_NO_PAD.decode(x).ok()), - min_pin_length: incoming_extensions.min_pin_length, - cred_protect: match incoming_extensions.credential_protection_policy { - Some(cred_prot_policy) => Some(CredentialProtectionExtension { - policy: cred_prot_policy, - enforce_policy: incoming_extensions - .enforce_credential_protection_policy - .unwrap_or_default(), - }), - None => None, - }, - large_blob: incoming_extensions - .large_blob - .map(|x| x.support.unwrap_or_default()) - .unwrap_or_default(), - hmac_or_prf: if incoming_extensions.prf.is_some() { - // CTAP currently doesn't support PRF queries at credentials.create() - // So we ignore any potential value set in the request and only mark this - // credential to activate HMAC for future PRF queries using credentials.get() - MakeCredentialHmacOrPrfInput::Prf - } else { - // MakeCredentialHmacOrPrfInput::Hmac is not used directly by webauthn - MakeCredentialHmacOrPrfInput::None - }, - }; - Some(extensions) + (resident_key, user_verification) } else { - None - }; - - let credential_parameters = request_value - .clone() - .get("pubKeyCredParams") - .ok_or_else(|| { - webauthn::Error::Internal( - "Request JSON missing or invalid `pubKeyCredParams` key".to_string(), - ) - }) - .and_then(|val| -> std::result::Result, webauthn::Error> { - serde_json::from_str::>(&val.to_string()) - .map_err(|e| { - webauthn::Error::Internal(format!( - "Request JSON missing or invalid `pubKeyCredParams` key: {e}" - )) - }) - })?; - let algorithms = credential_parameters - .iter() - .filter_map(|p| p.try_into().ok()) - .collect(); - let exclude = other_options.excluded_credentials.map(|v| { - v.iter() - .map(|e| e.try_into()) - .filter_map(|e| e.ok()) - .collect() - }); - let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { - (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), - (Some(origin), None) => (origin.to_string(), true), - // origin should always be set on request either by client or D-Bus service, - // so this shouldn't be called - (None, _) => { - return Err(webauthn::Error::Internal( - "Error reading origin from request".to_string(), - )); - } + (None, UserVerificationRequirement::Preferred) }; - let client_data_json = webauthn::format_client_data_json( - Operation::Create { - cred_type: CredentialType::Passkey, + let extensions = if let Some(incoming_extensions) = other_options.extensions { + let extensions = MakeCredentialsRequestExtensions { + cred_props: incoming_extensions.cred_props, + cred_blob: incoming_extensions + .cred_blob + .and_then(|x| URL_SAFE_NO_PAD.decode(x).ok()), + min_pin_length: incoming_extensions.min_pin_length, + cred_protect: match incoming_extensions.credential_protection_policy { + Some(cred_prot_policy) => Some(CredentialProtectionExtension { + policy: cred_prot_policy, + enforce_policy: incoming_extensions + .enforce_credential_protection_policy + .unwrap_or_default(), + }), + None => None, }, - &challenge, - &origin, - is_cross_origin, - ); - let client_data_hash = webauthn::create_client_data_hash(&client_data_json); - Ok(( - MakeCredentialRequest { - hash: client_data_hash, - origin, - - relying_party: rp, - user, - resident_key, - user_verification, - algorithms, - exclude, - extensions, - timeout: other_options.timeout.unwrap_or(Duration::from_secs(300)), + large_blob: incoming_extensions + .large_blob + .map(|x| x.support.unwrap_or_default()) + .unwrap_or_default(), + hmac_or_prf: if incoming_extensions.prf.is_some() { + // CTAP currently doesn't support PRF queries at credentials.create() + // So we ignore any potential value set in the request and only mark this + // credential to activate HMAC for future PRF queries using credentials.get() + MakeCredentialHmacOrPrfInput::Prf + } else { + // MakeCredentialHmacOrPrfInput::Hmac is not used directly by webauthn + MakeCredentialHmacOrPrfInput::None }, - client_data_json, - )) - } -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreateCredentialResponse { - #[zvariant(rename = "type")] - r#type: String, - public_key: Option, -} - -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreatePublicKeyCredentialRequest { - pub(crate) request_json: String, -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct CreatePublicKeyCredentialResponse { - registration_response_json: String, -} - -impl CreatePublicKeyCredentialResponse { - pub(super) fn try_from_ctap2_response( - response: &MakeCredentialResponseInternal, - client_data_json: String, - ) -> std::result::Result { - let auth_data = &response.ctap.authenticator_data; - let attested_credential = auth_data.attested_credential.as_ref().ok_or_else(|| { - fdo::Error::Failed("Invalid credential received from authenticator".to_string()) - })?; - - let unsigned_extensions = - serde_json::to_string(&response.ctap.unsigned_extensions_output).unwrap(); - let authenticator_data_blob = auth_data.to_response_bytes().unwrap(); - let attestation_statement = - (&response.ctap.attestation_statement) - .try_into() - .map_err(|_| { - fdo::Error::Failed("Could not serialize attestation statement".to_string()) - })?; - let attestation_object = webauthn::create_attestation_object( - &authenticator_data_blob, - &attestation_statement, - response.ctap.enterprise_attestation.unwrap_or(false), - ) - .map_err(|_| zbus::Error::Failure("Failed to create attestation object".to_string()))?; - // do we need to check that the client_data_hash is the same? - let registration_response_json = webauthn::CreatePublicKeyCredentialResponse::new( - attested_credential.credential_id.clone(), - attestation_object, - client_data_json, - Some(response.transport.clone()), - unsigned_extensions, - response.attachment_modality.clone(), - ) - .to_json(); - let response = CreatePublicKeyCredentialResponse { - registration_response_json, }; - Ok(response) - } -} - -impl From for CreateCredentialResponse { - fn from(response: CreatePublicKeyCredentialResponse) -> Self { - CreateCredentialResponse { - // TODO: Decide on camelCase or kebab-case for cred types - r#type: "public-key".to_string(), - public_key: Some(response), + Some(extensions) + } else { + None + }; + + let credential_parameters = request_value + .clone() + .get("pubKeyCredParams") + .ok_or_else(|| { + webauthn::Error::Internal( + "Request JSON missing or invalid `pubKeyCredParams` key".to_string(), + ) + }) + .and_then(|val| -> std::result::Result, webauthn::Error> { + serde_json::from_str::>(&val.to_string()).map_err( + |e| { + webauthn::Error::Internal(format!( + "Request JSON missing or invalid `pubKeyCredParams` key: {e}" + )) + }, + ) + })?; + let algorithms = credential_parameters + .iter() + .filter_map(|p| p.try_into().ok()) + .collect(); + let exclude = other_options.excluded_credentials.map(|v| { + v.iter() + .map(|e| e.try_into()) + .filter_map(|e| e.ok()) + .collect() + }); + let (origin, is_cross_origin) = match (request.origin.as_ref(), request.is_same_origin.as_ref()) + { + (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), + (Some(origin), None) => (origin.to_string(), true), + // origin should always be set on request either by client or D-Bus service, + // so this shouldn't be called + (None, _) => { + return Err(webauthn::Error::Internal( + "Error reading origin from request".to_string(), + )); } + }; + let client_data_json = + webauthn::format_client_data_json(Operation::Create, &challenge, &origin, is_cross_origin); + let client_data_hash = webauthn::create_client_data_hash(&client_data_json); + Ok(( + MakeCredentialRequest { + hash: client_data_hash, + origin, + + relying_party: rp, + user, + resident_key, + user_verification, + algorithms, + exclude, + extensions, + timeout: other_options.timeout.unwrap_or(Duration::from_secs(300)), + }, + client_data_json, + )) +} + +pub(super) fn create_credential_response_try_from_ctap2( + response: &MakeCredentialResponseInternal, + client_data_json: String, +) -> std::result::Result { + let auth_data = &response.ctap.authenticator_data; + let attested_credential = auth_data.attested_credential.as_ref().ok_or_else(|| { + fdo::Error::Failed("Invalid credential received from authenticator".to_string()) + })?; + + let unsigned_extensions = + serde_json::to_string(&response.ctap.unsigned_extensions_output).unwrap(); + let authenticator_data_blob = auth_data.to_response_bytes().unwrap(); + let attestation_statement = (&response.ctap.attestation_statement) + .try_into() + .map_err(|_| fdo::Error::Failed("Could not serialize attestation statement".to_string()))?; + let attestation_object = webauthn::create_attestation_object( + &authenticator_data_blob, + &attestation_statement, + response.ctap.enterprise_attestation.unwrap_or(false), + ) + .map_err(|_| zbus::Error::Failure("Failed to create attestation object".to_string()))?; + // do we need to check that the client_data_hash is the same? + let registration_response_json = webauthn::CreatePublicKeyCredentialResponse::new( + attested_credential.credential_id.clone(), + attestation_object, + client_data_json, + Some(response.transport.clone()), + unsigned_extensions, + response.attachment_modality.clone(), + ) + .to_json(); + let response = CreatePublicKeyCredentialResponse { + registration_response_json, + }; + Ok(response) +} + +pub(super) fn get_credential_request_try_into_ctap2( + request: &GetCredentialRequest, +) -> std::result::Result<(GetAssertionRequest, String), webauthn::Error> { + if request.public_key.is_none() { + return Err(webauthn::Error::NotSupported); } -} - -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetCredentialRequest { - pub(super) origin: Option, - pub(super) is_same_origin: Option, - #[zvariant(rename = "type")] - pub(super) r#type: String, - #[zvariant(rename = "publicKey")] - pub(super) public_key: Option, -} - -impl GetCredentialRequest { - pub(super) fn try_into_ctap2_request( - &self, - ) -> std::result::Result<(GetAssertionRequest, String), webauthn::Error> { - if self.public_key.is_none() { - return Err(webauthn::Error::NotSupported); - } - let options = self.public_key.as_ref().unwrap(); - let request: webauthn::GetCredentialOptions = - serde_json::from_str(&options.request_json) - .map_err(|e| webauthn::Error::Internal(format!("Invalid request JSON: {:?}", e)))?; - let mut allow: Vec = request - .allow_credentials - .iter() - .filter_map(|cred| { - if cred.cred_type == "public-key" { - cred.try_into().ok() - } else { - None - } - }) - .collect(); - // TODO: The allow is returning an empty list instead of either None or a list of transports. - // This should be investigated, but this is just a UI hint and isn't necessary to pass to the authenticator. - // Just removing it for now. - for c in allow.iter_mut() { - c.transports = None; - } - let (origin, is_cross_origin) = match (self.origin.as_ref(), self.is_same_origin.as_ref()) { - (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), - (Some(origin), None) => (origin.to_string(), true), - // origin should always be set on request either by client or D-Bus service, - // so this shouldn't be called - (None, _) => { - return Err(webauthn::Error::Internal( - "Error reading origin from request".to_string(), - )); - } - }; - let client_data_json = webauthn::format_client_data_json( - Operation::Get { - cred_types: vec![CredentialType::Passkey], - }, - &request.challenge, - &origin, - is_cross_origin, - ); - 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 request - .user_verification - .unwrap_or_else(|| String::from("preferred")) - .as_ref() - { - "required" => UserVerificationRequirement::Required, - "preferred" => UserVerificationRequirement::Preferred, - "discouraged" => UserVerificationRequirement::Discouraged, - _ => { - return Err(webauthn::Error::Internal( - "Invalid user verification requirement specified".to_string(), - )) - } - }; - let relying_party_id = request.rp_id.unwrap_or_else(|| { - let (_, effective_domain) = origin.rsplit_once('/').unwrap(); - effective_domain.to_string() - }); - - let extensions = if let Some(incoming_extensions) = request.extensions { - let extensions = GetAssertionRequestExtensions { - cred_blob: incoming_extensions.get_cred_blob, - hmac_or_prf: incoming_extensions - .prf - .and_then(|x| { - x.eval.map(|eval| { - let eval = Some(eval.decode()); - let mut eval_by_credential = HashMap::new(); - if let Some(incoming_eval) = x.eval_by_credential { - for (key, val) in incoming_eval.iter() { - eval_by_credential.insert(key.clone(), val.decode()); - } - } - GetAssertionHmacOrPrfInput::Prf { - eval, - eval_by_credential, - } - }) - }) - .unwrap_or_default(), - large_blob: incoming_extensions - .large_blob - // TODO: Implement GetAssertionLargeBlobExtension::Write, once libwebauthn supports it - .filter(|x| x.read == Some(true)) - .map(|_| GetAssertionLargeBlobExtension::Read) - .unwrap_or(GetAssertionLargeBlobExtension::None), - }; - Some(extensions) - } else { - None - }; - - Ok(( - GetAssertionRequest { - hash: client_data_hash, - relying_party_id, - user_verification, - allow, - extensions, - timeout: request.timeout.unwrap_or(Duration::from_secs(300)), - }, - client_data_json, + let options: webauthn::GetCredentialOptions = request + .public_key + .as_ref() + .ok_or(webauthn::Error::Internal( + ("Invalid request: no \"public-key\" options specified").to_string(), )) - } -} - -#[derive(Clone, Debug, DeserializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetPublicKeyCredentialRequest { - pub(crate) request_json: String, -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetCredentialResponse { - #[zvariant(rename = "type")] - r#type: String, - public_key: Option, -} - -#[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] -pub struct GetPublicKeyCredentialResponse { - authentication_response_json: String, -} - -impl GetPublicKeyCredentialResponse { - pub(super) fn try_from_ctap2_response( - response: &GetAssertionResponseInternal, - client_data_json: String, - ) -> std::result::Result { - let authenticator_data_blob = response - .ctap - .authenticator_data - .to_response_bytes() - .unwrap(); - - // We can't just do this here, because we need encode all byte arrays for the JS-communication: - // let unsigned_extensions = response - // .ctap - // .unsigned_extensions_output - // .as_ref() - // .map(|extensions| serde_json::to_string(&extensions).unwrap()); - let unsigned_extensions = response - .ctap - .unsigned_extensions_output - .as_ref() - .map(GetPublicKeyCredentialUnsignedExtensionsResponse::from); - - let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( - client_data_json, - response - .ctap - .credential_id - .as_ref() - .map(|c| c.id.clone().into_vec()), - authenticator_data_blob, - response.ctap.signature.clone(), - response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), - response.attachment_modality.clone(), - unsigned_extensions, - ) - .to_json(); - - let response = GetPublicKeyCredentialResponse { - authentication_response_json, - }; - Ok(response) - } -} - -impl From for GetCredentialResponse { - fn from(response: GetPublicKeyCredentialResponse) -> Self { - GetCredentialResponse { - // TODO: Decide on camelCase or kebab-case for cred types - r#type: "public-key".to_string(), - public_key: Some(response), - } - } -} - -/// Updates to send to the client -#[derive(Serialize, Deserialize, Type)] -pub enum ClientUpdate { - SetTitle(OwnedValue), - SetDevices(OwnedValue), - SetCredentials(OwnedValue), - - WaitingForDevice(OwnedValue), - SelectingDevice(OwnedValue), - - UsbNeedsPin(OwnedValue), - UsbNeedsUserVerification(OwnedValue), - UsbNeedsUserPresence(OwnedValue), - - HybridNeedsQrCode(OwnedValue), - HybridConnecting(OwnedValue), - HybridConnected(OwnedValue), - - Completed(OwnedValue), - Failed(OwnedValue), -} - -impl TryFrom for ViewUpdate { - type Error = zbus::zvariant::Error; - fn try_from(value: ClientUpdate) -> std::result::Result { - match value { - ClientUpdate::SetTitle(v) => v.try_into().map(Self::SetTitle), - ClientUpdate::SetDevices(v) => { - let dbus_devices: Vec = Value::<'_>::from(v).try_into()?; - let devices: std::result::Result, zbus::zvariant::Error> = - dbus_devices - .into_iter() - .map(|d| { - d.try_into().map_err(|_| { - zbus::zvariant::Error::Message( - "Could not deserialize devices".to_string(), - ) - }) - }) - .collect(); - Ok(Self::SetDevices(devices?)) - } - ClientUpdate::SetCredentials(v) => { - let dbus_credentials: Vec = Value::<'_>::from(v).try_into()?; - let credentials: std::result::Result< - Vec, - zbus::zvariant::Error, - > = dbus_credentials - .into_iter() - .map(|creds| Ok(creds.into())) - .collect(); - Ok(Self::SetCredentials(credentials?)) - } - - ClientUpdate::WaitingForDevice(v) => { - let dbus_device: Device = Value::<'_>::from(v).try_into()?; - let device: crate::model::Device = dbus_device.try_into().map_err(|_| { - zbus::zvariant::Error::Message("Could not deserialize device".to_string()) - })?; - Ok(Self::WaitingForDevice(device)) - } - ClientUpdate::SelectingDevice(_) => Ok(Self::SelectingDevice), - - ClientUpdate::UsbNeedsPin(v) => v.try_into().map(|x: i32| { - let attempts_left = if x == -1 { None } else { Some(x as u32) }; - Self::UsbNeedsPin { attempts_left } - }), - ClientUpdate::UsbNeedsUserVerification(v) => v.try_into().map(|x: i32| { - let attempts_left = if x == -1 { None } else { Some(x as u32) }; - Self::UsbNeedsUserVerification { attempts_left } - }), - ClientUpdate::UsbNeedsUserPresence(_) => Ok(Self::UsbNeedsUserPresence), - - ClientUpdate::HybridNeedsQrCode(v) => v.try_into().map(Self::HybridNeedsQrCode), - ClientUpdate::HybridConnecting(_) => Ok(Self::HybridConnecting), - ClientUpdate::HybridConnected(_) => Ok(Self::HybridConnected), - - ClientUpdate::Completed(_) => Ok(Self::Completed), - ClientUpdate::Failed(v) => v.try_into().map(Self::Failed), - } - } -} - -#[derive(SerializeDict, DeserializeDict, Type)] -pub(super) struct Credential { - id: String, - name: String, - username: String, -} - -impl From for crate::model::Credential { - fn from(value: Credential) -> Self { - Self { - id: value.id, - name: value.name, - username: if value.username.is_empty() { - None + .and_then(|o| { + serde_json::from_str(&o.request_json) + .map_err(|e| webauthn::Error::Internal(format!("Invalid request JSON: {:?}", e))) + }) + .unwrap(); + let mut allow: Vec = options + .allow_credentials + .iter() + .filter_map(|cred| { + if cred.cred_type == "public-key" { + cred.try_into().ok() } else { - Some(value.username) - }, - } - } -} - -impl TryFrom> for Credential { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let credential: Credential = encoded.deserialize()?.0; - Ok(credential) - } -} - -#[derive(SerializeDict, DeserializeDict, Type)] -pub(super) struct Device { - id: String, - transport: String, -} - -impl TryFrom> for Device { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let device: Device = encoded.deserialize()?.0; - Ok(device) - } -} - -impl TryFrom for crate::model::Device { - type Error = (); - fn try_from(value: Device) -> std::result::Result { - let transport = value.transport.try_into().map_err(|_| ())?; - Ok(Self { - id: value.id, - transport, + None + } }) + .collect(); + // TODO: The allow is returning an empty list instead of either None or a list of transports. + // This should be investigated, but this is just a UI hint and isn't necessary to pass to the authenticator. + // Just removing it for now. + for c in allow.iter_mut() { + c.transports = None; } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Type)] -pub(super) enum HybridState { - /// Default state, not listening for hybrid transport. - Idle(OwnedValue), - - /// QR code flow is starting, awaiting QR code scan and BLE advert from phone. - Started(OwnedValue), - - /// BLE advert received, connecting to caBLE tunnel with shared secret. - Connecting(OwnedValue), - - /// Connected to device via caBLE tunnel. - Connected(OwnedValue), - - /// Credential received over tunnel. - Completed(OwnedValue), - - // This isn't actually sent from the server. - UserCancelled(OwnedValue), - - /// Failed to receive a credential - Failed(OwnedValue), -} - -impl TryFrom for crate::model::HybridState { - type Error = zbus::zvariant::Error; - fn try_from(value: HybridState) -> std::result::Result { - match value { - HybridState::Idle(_) => Ok(Self::Idle), - HybridState::Started(value) => value.try_into().map(Self::Started), - HybridState::Connecting(_) => Ok(Self::Connecting), - HybridState::Connected(_) => Ok(Self::Connected), - HybridState::Completed(_) => Ok(Self::Completed), - HybridState::UserCancelled(_) => Ok(Self::UserCancelled), - HybridState::Failed(_) => Ok(Self::Failed), + let (origin, is_cross_origin) = match (request.origin.as_ref(), request.is_same_origin.as_ref()) + { + (Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin), + (Some(origin), None) => (origin.to_string(), true), + // origin should always be set on request either by client or D-Bus service, + // so this shouldn't be called + (None, _) => { + return Err(webauthn::Error::Internal( + "Error reading origin from request".to_string(), + )); } - } -} - -impl TryFrom> for HybridState { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let obj: Self = encoded.deserialize()?.0; - Ok(obj) - } -} - -#[derive(Serialize, Deserialize, Type)] -pub(super) enum ServiceError { - /// Some unknown error with the authenticator occurred. - AuthenticatorError, - - /// No matching credentials were found on the device. - NoCredentials, - - /// Too many incorrect PIN attempts, and authenticator must be removed and - /// reinserted to continue any more PIN attempts. - /// - /// Note that this is different than exhausting the PIN count that fully - /// locks out the device. - PinAttemptsExhausted, - - // TODO: We may want to hide the details on this variant from the public API. - /// Something went wrong with the credential service itself, not the authenticator. - Internal, -} - -impl TryFrom> for ServiceError { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let obj: Self = encoded.deserialize()?.0; - Ok(obj) - } -} - -impl From for crate::model::Error { - fn from(value: ServiceError) -> Self { - match value { - ServiceError::AuthenticatorError => Self::AuthenticatorError, - ServiceError::NoCredentials => Self::NoCredentials, - ServiceError::PinAttemptsExhausted => Self::PinAttemptsExhausted, - // TODO: this is bogus, we should refactor to remove the tuple field - // and let the client decide how to render the error. - ServiceError::Internal => { - Self::Internal("Something went wrong. Please try again later.".to_string()) - } + }; + let client_data_json = webauthn::format_client_data_json( + Operation::Get, + &options.challenge, + &origin, + is_cross_origin, + ); + 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 + .user_verification + .unwrap_or_else(|| String::from("preferred")) + .as_ref() + { + "required" => UserVerificationRequirement::Required, + "preferred" => UserVerificationRequirement::Preferred, + "discouraged" => UserVerificationRequirement::Discouraged, + _ => { + return Err(webauthn::Error::Internal( + "Invalid user verification requirement specified".to_string(), + )); } - } -} - -/// Used to de-/serialize state D-Bus and model::UsbState. -#[derive(Serialize, Deserialize, Type)] -pub(super) enum UsbState { - Idle(OwnedValue), - Waiting(OwnedValue), - SelectingDevice(OwnedValue), - Connected(OwnedValue), - NeedsPin(OwnedValue), /* { - attempts_left: Option, - }, - */ - NeedsUserVerification(OwnedValue), /* { - attempts_left: Option, - },*/ - - NeedsUserPresence(OwnedValue), - //UserCancelled, - SelectCredential(OwnedValue), /* { - creds: Vec, - },*/ - Completed(OwnedValue), - // Failed(crate::credential_service::Error), - Failed(OwnedValue), -} - -impl TryFrom for crate::model::UsbState { - type Error = zbus::zvariant::Error; - fn try_from(value: UsbState) -> std::result::Result { - let ret = match value { - UsbState::Idle(_) => Ok(Self::Idle), - UsbState::Waiting(_) => Ok(Self::Waiting), - UsbState::SelectingDevice(_) => Ok(Self::SelectingDevice), - UsbState::Connected(_) => Ok(Self::Connected), - UsbState::NeedsPin(value) => value.try_into().map(|attempts_left: i32| { - let attempts_left = if attempts_left < 0 { - None - } else { - Some(u32::try_from(attempts_left).unwrap()) - }; - Self::NeedsPin { attempts_left } - }), - UsbState::NeedsUserVerification(value) => value.try_into().map(|attempts_left: i32| { - let attempts_left = if attempts_left < 0 { - None - } else { - Some(u32::try_from(attempts_left).unwrap()) - }; - Self::NeedsUserVerification { attempts_left } - }), - UsbState::NeedsUserPresence(_) => Ok(Self::NeedsUserPresence), - UsbState::SelectCredential(value) => value - .try_into() - .map(|creds: Vec| { - creds - .into_iter() - .map(crate::model::Credential::from) - .collect() + }; + let relying_party_id = options.rp_id.unwrap_or_else(|| { + let (_, effective_domain) = origin.rsplit_once('/').unwrap(); + effective_domain.to_string() + }); + + let extensions = if let Some(incoming_extensions) = options.extensions { + let extensions = GetAssertionRequestExtensions { + cred_blob: incoming_extensions.get_cred_blob, + hmac_or_prf: incoming_extensions + .prf + .and_then(|x| { + x.eval.map(|eval| { + let eval = Some(eval.decode()); + let mut eval_by_credential = HashMap::new(); + if let Some(incoming_eval) = x.eval_by_credential { + for (key, val) in incoming_eval.iter() { + eval_by_credential.insert(key.clone(), val.decode()); + } + } + GetAssertionHmacOrPrfInput::Prf { + eval, + eval_by_credential, + } + }) }) - .map(|creds| Self::SelectCredential { creds }), - UsbState::Completed(_) => Ok(Self::Completed), - UsbState::Failed(value) => { - ServiceError::try_from(Value::<'_>::from(value)).map(|err| Self::Failed(err.into())) - } - }?; - Ok(ret) - } -} - -impl TryFrom> for UsbState { - type Error = zbus::zvariant::Error; - fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zbus::zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zbus::zvariant::to_bytes(ctx, &value)?; - let obj: Self = encoded.deserialize()?.0; - Ok(obj) - } + .unwrap_or_default(), + large_blob: incoming_extensions + .large_blob + // TODO: Implement GetAssertionLargeBlobExtension::Write, once libwebauthn supports it + .filter(|x| x.read == Some(true)) + .map(|_| GetAssertionLargeBlobExtension::Read) + .unwrap_or(GetAssertionLargeBlobExtension::None), + }; + Some(extensions) + } else { + None + }; + + Ok(( + GetAssertionRequest { + hash: client_data_hash, + relying_party_id, + user_verification, + allow, + extensions, + timeout: options.timeout.unwrap_or(Duration::from_secs(300)), + }, + client_data_json, + )) +} + +pub(super) fn get_credential_response_try_from_ctap2( + response: &GetAssertionResponseInternal, + client_data_json: String, +) -> std::result::Result { + let authenticator_data_blob = response + .ctap + .authenticator_data + .to_response_bytes() + .unwrap(); + + // We can't just do this here, because we need encode all byte arrays for the JS-communication: + // let unsigned_extensions = response + // .ctap + // .unsigned_extensions_output + // .as_ref() + // .map(|extensions| serde_json::to_string(&extensions).unwrap()); + let unsigned_extensions = response + .ctap + .unsigned_extensions_output + .as_ref() + .map(GetPublicKeyCredentialUnsignedExtensionsResponse::from); + + let authentication_response_json = webauthn::GetPublicKeyCredentialResponse::new( + client_data_json, + response + .ctap + .credential_id + .as_ref() + .map(|c| c.id.clone().into_vec()), + authenticator_data_blob, + response.ctap.signature.clone(), + response.ctap.user.as_ref().map(|u| u.id.clone().into_vec()), + response.attachment_modality.clone(), + unsigned_extensions, + ) + .to_json(); + + let response = GetPublicKeyCredentialResponse { + authentication_response_json, + }; + Ok(response) } diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs index e7d31e7c..dd75101d 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs @@ -2,7 +2,7 @@ mod cbor; mod cose; mod credential_service; mod dbus; -mod model; +// mod model; mod serde; mod webauthn; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs index 03c21e10..ca5052a5 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs @@ -4,7 +4,8 @@ use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use libwebauthn::{ ops::webauthn::{CredentialProtectionPolicy, MakeCredentialLargeBlobExtension}, proto::ctap2::{ - Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialType, Ctap2Transport, + Ctap2AttestationStatement, Ctap2CredentialType, Ctap2PublicKeyCredentialType, + Ctap2Transport, }, }; use ring::digest; @@ -12,7 +13,9 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::debug; -use crate::{cose::{CoseKeyAlgorithmIdentifier, CoseKeyType}, model::Operation}; +use creds_lib::model::Operation; + +use crate::cose::{CoseKeyAlgorithmIdentifier, CoseKeyType}; pub use libwebauthn::ops::webauthn::{ Assertion, CredentialProtectionExtension, GetAssertionHmacOrPrfInput, @@ -669,8 +672,8 @@ impl GetPublicKeyCredentialResponse { pub fn create_client_data_hash(json: &str) -> Vec { digest::digest(&digest::SHA256, json.as_bytes()) - .as_ref() - .to_owned() + .as_ref() + .to_owned() } pub fn format_client_data_json( From 275413a66aef793b309b4c7e142cb175e7bd73b6 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 1 Aug 2025 08:08:20 -0500 Subject: [PATCH 17/38] Change build target directories --- .vscode/launch.json | 2 +- creds-ui/meson.build | 2 +- creds-ui/src/meson.build | 6 ++---- xyz-iinuwa-credential-manager-portal-gtk/meson.build | 4 +++- xyz-iinuwa-credential-manager-portal-gtk/src/meson.build | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8a9c2f98..e0c9f6d5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "type": "lldb", "request": "launch", "name": "Debug executable 'xicm-portal-gtk'", - "program": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/debug/xyz-iinuwa-credential-manager-portal-gtk", + "program": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/src/xyzii-credman", "args": [], "env": { "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/data", diff --git a/creds-ui/meson.build b/creds-ui/meson.build index 30e4ef75..cff09cf5 100644 --- a/creds-ui/meson.build +++ b/creds-ui/meson.build @@ -1,7 +1,7 @@ i18n = import('i18n') gnome = import('gnome') -gui_executable_name = 'xyzii-creds-portal-gtk' +gui_executable_name = 'creds-ui' gui_build_dir = meson.current_build_dir() gui_source_dir = meson.current_source_dir() base_id = 'xyz.iinuwa.CredentialManagerUi' diff --git a/creds-ui/src/meson.build b/creds-ui/src/meson.build index bf264612..c2cc648d 100644 --- a/creds-ui/src/meson.build +++ b/creds-ui/src/meson.build @@ -1,5 +1,3 @@ -gui_cargo_package_name = 'creds-ui' - global_conf = configuration_data() global_conf.set_quoted('APP_ID', application_id) if (get_option('profile') == 'development') @@ -27,7 +25,7 @@ run_command( cargo_options = [ '--manifest-path', meson.project_source_root() / gui_source_dir / 'Cargo.toml', ] -cargo_options += ['--target-dir', meson.project_build_root() / gui_build_dir] +cargo_options += ['--target-dir', meson.project_build_root() / gui_build_dir / 'target'] if get_option('profile') == 'default' cargo_options += ['--release'] @@ -57,7 +55,7 @@ custom_target( cargo_options, '&&', 'cp', - gui_build_dir / rust_target / gui_cargo_package_name, + gui_build_dir / 'target' / rust_target / gui_executable_name, '@OUTPUT@', ], ) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/meson.build index 7d8d4ad1..dd1870b2 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/meson.build @@ -45,7 +45,9 @@ endif cargo_options = [ '--manifest-path', meson.project_source_root() / meson.current_source_dir() / 'Cargo.toml', ] -cargo_options += ['--target-dir', meson.project_build_root() / meson.current_build_dir()] +cargo_options += [ + '--target-dir', meson.project_build_root() / meson.current_build_dir() / 'target', +] subdir('src') subdir('tests') diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build index 062af136..434fee12 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build @@ -26,7 +26,7 @@ custom_target( cargo_options, '&&', 'cp', - backend_build_dir / rust_target / backend_executable_name, + backend_build_dir / 'target' / rust_target / backend_executable_name, '@OUTPUT@', ], ) From 23d347550d8e0f5aac691b6cd8b73dd9a53012bc Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 1 Aug 2025 08:37:43 -0500 Subject: [PATCH 18/38] Comment out creds-lib meson files --- creds-lib/meson.build | 64 +++++++++++++++++++++--------------- creds-lib/src/meson.build | 68 +++++++++++++++++++-------------------- 2 files changed, 71 insertions(+), 61 deletions(-) diff --git a/creds-lib/meson.build b/creds-lib/meson.build index 6eda8812..d1462102 100644 --- a/creds-lib/meson.build +++ b/creds-lib/meson.build @@ -1,27 +1,39 @@ -common_lib_name = 'xyzii-credman-portal-gtk' -base_id = 'xyz.iinuwa.CredentialManagerUi' +# Currently, we're not building this with meson and just letting cargo path dependencies for the other projects build this lib. +# Not efficient, since the UI and daemon projects will build it separately, but leaving it this way for now. -cargo = find_program('cargo', required: true) - -version = meson.project_version() - -if get_option('profile') == 'development' - profile = 'Devel' - vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() - if vcs_tag == '' - version_suffix = '-devel' - else - version_suffix = '-@0@'.format(vcs_tag) - endif - application_id = '@0@.@1@'.format(base_id, profile) -else - profile = '' - version_suffix = '' - application_id = base_id -endif - -meson.add_dist_script( - meson.project_source_root() / 'build-aux/dist-vendor.sh', - meson.project_build_root() / 'meson-dist' / common_lib_name + '-' + version, - meson.project_source_root(), -) +# common_lib_name = 'xyzii-credman-portal-gtk' +# base_id = 'xyz.iinuwa.CredentialManagerUi' +# +# cargo = find_program('cargo', required: true) +# +# version = meson.project_version() +# +# if get_option('profile') == 'development' +# profile = 'Devel' +# vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() +# if vcs_tag == '' +# version_suffix = '-devel' +# else +# version_suffix = '-@0@'.format(vcs_tag) +# endif +# application_id = '@0@.@1@'.format(base_id, profile) +# else +# profile = '' +# version_suffix = '' +# application_id = base_id +# endif +# +# meson.add_dist_script( +# meson.project_source_root() / 'build-aux/dist-vendor.sh', +# meson.project_build_root() / 'meson-dist' / common_lib_name + '-' + version, +# meson.project_source_root(), +# ) +# +# cargo_options = [ +# '--manifest-path', meson.project_source_root() / meson.current_source_dir() / 'Cargo.toml', +# ] +# cargo_options += [ +# '--target-dir', meson.project_build_root() / meson.current_build_dir() / 'target', +# ] +# +# subdir('src') diff --git a/creds-lib/src/meson.build b/creds-lib/src/meson.build index 45544aa0..441a13a9 100644 --- a/creds-lib/src/meson.build +++ b/creds-lib/src/meson.build @@ -1,36 +1,34 @@ -cargo_options = [ - '--manifest-path', meson.project_source_root() / meson.current_source_dir() / '..' / 'Cargo.toml', -] -cargo_options += ['--target-dir', meson.project_build_root() / meson.current_build_dir()] +# Currently, we're not building this with meson and just letting cargo path dependencies for the other projects build this lib. +# Not efficient, since the UI and daemon projects will build it separately, but leaving it this way for now. -if get_option('profile') == 'default' - cargo_options += ['--release'] - rust_target = 'release' - message('Building in release mode') -else - rust_target = 'debug' - message('Building in debug mode') -endif - -cargo_env = ['CARGO_HOME=' + meson.project_build_root() / 'cargo-home'] - -custom_target( - 'cargo-build', - build_by_default: true, - build_always_stale: true, - output: common_lib_name, - console: true, - install: true, - install_dir: bindir, - command: [ - 'env', - cargo_env, - cargo, - 'build', - cargo_options, - '&&', - 'cp', - common_lib_name / 'src' / rust_target / common_lib_name, - '@OUTPUT@', - ], -) +# if get_option('profile') == 'default' +# cargo_options += ['--release'] +# rust_target = 'release' +# message('Building in release mode') +# else +# rust_target = 'debug' +# message('Building in debug mode') +# endif +# +# cargo_env = ['CARGO_HOME=' + meson.project_build_root() / 'cargo-home'] +# +# custom_target( +# 'cargo-build', +# build_by_default: true, +# build_always_stale: true, +# output: common_lib_name, +# console: true, +# install: true, +# install_dir: bindir, +# command: [ +# 'env', +# cargo_env, +# cargo, +# 'build', +# cargo_options, +# '&&', +# 'cp', +# common_lib_name / 'src' / rust_target / common_lib_name, +# '@OUTPUT@', +# ], +# ) From 0155dbeb3c06cc41cd90838bcdb00ef7e7cb9ed6 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 1 Aug 2025 08:37:43 -0500 Subject: [PATCH 19/38] Rename daemon to credsd --- .vscode/launch.json | 19 ++++++-- .../.gitignore | 0 .../Cargo.lock | 46 +++++++++---------- .../Cargo.toml | 2 +- .../meson.build | 2 +- .../src/cbor.rs | 0 .../src/cose.rs | 0 .../src/credential_service/hybrid.rs | 0 .../src/credential_service/mod.rs | 0 .../src/credential_service/server.rs | 0 .../src/credential_service/usb.rs | 0 .../src/dbus.rs | 0 .../src/dbus/model.rs | 0 .../src/main.rs | 0 .../src/meson.build | 0 .../src/serde/mod.rs | 0 .../src/webauthn.rs | 0 .../tests/config/mod.rs.in | 0 .../tests/dbus.rs | 0 .../tests/meson.build | 0 .../xyz.iinuwa.CredentialManagerUi.service.in | 0 meson.build | 2 +- 22 files changed, 42 insertions(+), 29 deletions(-) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/.gitignore (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/Cargo.lock (99%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/Cargo.toml (97%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/meson.build (97%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/cbor.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/cose.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/credential_service/hybrid.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/credential_service/mod.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/credential_service/server.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/credential_service/usb.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/dbus.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/dbus/model.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/main.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/meson.build (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/serde/mod.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/src/webauthn.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/tests/config/mod.rs.in (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/tests/dbus.rs (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/tests/meson.build (100%) rename {xyz-iinuwa-credential-manager-portal-gtk => credsd}/tests/services/xyz.iinuwa.CredentialManagerUi.service.in (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index e0c9f6d5..854e8594 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,12 +7,25 @@ { "type": "lldb", "request": "launch", - "name": "Debug executable 'xicm-portal-gtk'", - "program": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/src/xyzii-credman", + "name": "Debug Daemon (credsd)", + "program": "${workspaceFolder}/build/credsd/src/credsd", + "args": [], + "env": { + "RUST_LOG": "credsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug" + }, + "sourceLanguages": ["rust"], + "cwd": "${workspaceFolder}", + "preLaunchTask": "Meson: Build all targets" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug UI (creds-ui)", + "program": "${workspaceFolder}/build/creds-ui/src/creds-ui", "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,libwebauthn::transport::channel=debug" + "RUST_LOG": "creds_ui=debug,zbus::info" }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", diff --git a/xyz-iinuwa-credential-manager-portal-gtk/.gitignore b/credsd/.gitignore similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/.gitignore rename to credsd/.gitignore diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock b/credsd/Cargo.lock similarity index 99% rename from xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock rename to credsd/Cargo.lock index 895a954b..b16ab393 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock +++ b/credsd/Cargo.lock @@ -692,6 +692,29 @@ dependencies = [ "zbus", ] +[[package]] +name = "credsd" +version = "0.1.0" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "cosey", + "creds-lib", + "futures-lite", + "gio", + "libwebauthn", + "openssl", + "ring", + "rustls", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -3568,29 +3591,6 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" -[[package]] -name = "xyzii-credman" -version = "0.1.0" -dependencies = [ - "async-stream", - "async-trait", - "base64", - "cosey", - "creds-lib", - "futures-lite", - "gio", - "libwebauthn", - "openssl", - "ring", - "rustls", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - [[package]] name = "zbus" version = "5.9.0" diff --git a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml b/credsd/Cargo.toml similarity index 97% rename from xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml rename to credsd/Cargo.toml index 9167fc9c..731c2885 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml +++ b/credsd/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "xyzii-credman" +name = "credsd" version = "0.1.0" authors = ["Isaiah Inuwa "] edition = "2021" diff --git a/xyz-iinuwa-credential-manager-portal-gtk/meson.build b/credsd/meson.build similarity index 97% rename from xyz-iinuwa-credential-manager-portal-gtk/meson.build rename to credsd/meson.build index dd1870b2..7e2b9329 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/meson.build +++ b/credsd/meson.build @@ -1,4 +1,4 @@ -backend_executable_name = 'xyzii-credman' +backend_executable_name = 'credsd' backend_build_dir = meson.current_build_dir() base_id = 'xyz.iinuwa.CredentialManagerUi' diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/cbor.rs b/credsd/src/cbor.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/cbor.rs rename to credsd/src/cbor.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/cose.rs b/credsd/src/cose.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/cose.rs rename to credsd/src/cose.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs b/credsd/src/credential_service/hybrid.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/hybrid.rs rename to credsd/src/credential_service/hybrid.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs b/credsd/src/credential_service/mod.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs rename to credsd/src/credential_service/mod.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs b/credsd/src/credential_service/server.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/server.rs rename to credsd/src/credential_service/server.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs b/credsd/src/credential_service/usb.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/usb.rs rename to credsd/src/credential_service/usb.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/credsd/src/dbus.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs rename to credsd/src/dbus.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs b/credsd/src/dbus/model.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/dbus/model.rs rename to credsd/src/dbus/model.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/main.rs b/credsd/src/main.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/main.rs rename to credsd/src/main.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/meson.build b/credsd/src/meson.build similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/meson.build rename to credsd/src/meson.build diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/serde/mod.rs b/credsd/src/serde/mod.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/serde/mod.rs rename to credsd/src/serde/mod.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs b/credsd/src/webauthn.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/src/webauthn.rs rename to credsd/src/webauthn.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in b/credsd/tests/config/mod.rs.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in rename to credsd/tests/config/mod.rs.in diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs b/credsd/tests/dbus.rs similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs rename to credsd/tests/dbus.rs diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build b/credsd/tests/meson.build similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build rename to credsd/tests/meson.build diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in b/credsd/tests/services/xyz.iinuwa.CredentialManagerUi.service.in similarity index 100% rename from xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in rename to credsd/tests/services/xyz.iinuwa.CredentialManagerUi.service.in diff --git a/meson.build b/meson.build index 4f31c227..83848874 100644 --- a/meson.build +++ b/meson.build @@ -21,5 +21,5 @@ meson.add_dist_script( ) subdir('creds-lib') -subdir('xyz-iinuwa-credential-manager-portal-gtk') +subdir('credsd') subdir('creds-ui') From 191e1b07d9487c5b284103fc634eff17dcd36af3 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 1 Aug 2025 21:20:48 -0500 Subject: [PATCH 20/38] wip: Launch UI process over D-Bus --- .vscode/launch.json | 6 +- creds-lib/src/model.rs | 15 ++ creds-lib/src/server.rs | 270 +++++++++++++++++++- creds-ui/Cargo.lock | 67 +++++ creds-ui/Cargo.toml | 1 + creds-ui/src/client.rs | 100 ++++++++ creds-ui/src/dbus.rs | 9 +- creds-ui/src/main.rs | 115 +-------- credsd/src/credential_service/mod.rs | 61 +++-- credsd/src/credential_service/server.rs | 66 +++-- credsd/src/dbus.rs | 316 ++++++++++++++++++++---- credsd/src/main.rs | 35 ++- 12 files changed, 844 insertions(+), 217 deletions(-) create mode 100644 creds-ui/src/client.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 854e8594..70f665b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", - "preLaunchTask": "Meson: Build all targets" + "preLaunchTask": "Meson: Build Daemon" }, { "type": "lldb", @@ -25,11 +25,11 @@ "args": [], "env": { "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/data", - "RUST_LOG": "creds_ui=debug,zbus::info" + "RUST_LOG": "creds_ui=debug,zbus::trace" }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", - "preLaunchTask": "Meson: Build all targets" + "preLaunchTask": "Meson: Build UI" }, ] } diff --git a/creds-lib/src/model.rs b/creds-lib/src/model.rs index f6900ed6..c7a564c4 100644 --- a/creds-lib/src/model.rs +++ b/creds-lib/src/model.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; use zbus::zvariant::{SerializeDict, Type}; @@ -279,3 +281,16 @@ pub enum Error { /// Something went wrong with the credential service itself, not the authenticator. Internal(String), } + +impl std::error::Error for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AuthenticatorError => f.write_str("AuthenticatorError"), + Self::NoCredentials => f.write_str("NoCredentials"), + Self::PinAttemptsExhausted => f.write_str("PinAttemptsExhausted"), + Self::Internal(s) => write!(f, "InternalError: {s}"), + } + } +} diff --git a/creds-lib/src/server.rs b/creds-lib/src/server.rs index e79038ba..f876d102 100644 --- a/creds-lib/src/server.rs +++ b/creds-lib/src/server.rs @@ -1,9 +1,11 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use zbus::{ fdo, object_server::SignalEmitter, proxy, - zvariant::{self, DeserializeDict, LE, OwnedValue, SerializeDict, Type, Value}, + zvariant::{self, DeserializeDict, LE, Optional, OwnedValue, SerializeDict, Type, Value}, }; use crate::model::{Operation, ViewUpdate}; @@ -34,6 +36,30 @@ impl TryFrom for crate::model::BackgroundEvent { } } +impl TryFrom for BackgroundEvent { + type Error = zvariant::Error; + fn try_from(value: crate::model::BackgroundEvent) -> Result { + let event = match value { + crate::model::BackgroundEvent::HybridQrStateChanged(state) => { + let state: HybridState = state.into(); + let value = Value::new(state) + .try_to_owned() + .expect("non-file descriptor value to succeed"); + BackgroundEvent::HybridStateChanged(value) + } + crate::model::BackgroundEvent::UsbStateChanged(state) => { + let state: UsbState = state.into(); + let value = Value::new(state) + .try_to_owned() + .expect("non-file descriptor value to succeed"); + + BackgroundEvent::UsbStateChanged(value) + } + }; + Ok(event) + } +} + #[derive(Clone, Debug, DeserializeDict, Type)] #[zvariant(signature = "dict")] pub struct CreateCredentialRequest { @@ -75,11 +101,6 @@ impl From for CreateCredentialResponse { } } -#[derive(Serialize, Deserialize, Type)] -pub struct ViewRequest { - pub operation: Operation, -} - /// Updates to send to the client #[derive(Serialize, Deserialize, Type)] pub enum ClientUpdate { @@ -163,11 +184,12 @@ impl TryFrom for ViewUpdate { } } -#[derive(SerializeDict, DeserializeDict, Type)] +#[derive(SerializeDict, DeserializeDict, Type, Value)] +#[zvariant(signature = "dict")] pub struct Credential { id: String, name: String, - username: String, + username: Optional, } impl From for crate::model::Credential { @@ -175,15 +197,22 @@ impl From for crate::model::Credential { Self { id: value.id, name: value.name, - username: if value.username.is_empty() { - None - } else { - Some(value.username) - }, + username: value.username.into(), } } } +impl From for Credential { + fn from(value: crate::model::Credential) -> Self { + Self { + id: value.id, + name: value.name, + username: value.username.into(), + } + } +} + +/* impl TryFrom> for Credential { type Error = zvariant::Error; fn try_from(value: Value<'_>) -> std::result::Result { @@ -193,6 +222,7 @@ impl TryFrom> for Credential { Ok(credential) } } + */ #[derive(SerializeDict, DeserializeDict, Type)] pub struct Device { @@ -210,6 +240,15 @@ impl TryFrom> for Device { } } +impl From for Device { + fn from(value: crate::model::Device) -> Self { + Device { + id: value.id, + transport: value.transport.as_str().to_owned(), + } + } +} + impl TryFrom for crate::model::Device { type Error = (); fn try_from(value: Device) -> std::result::Result { @@ -286,6 +325,21 @@ pub enum HybridState { Failed(OwnedValue), } +impl From for HybridState { + fn from(value: crate::model::HybridState) -> Self { + match value { + crate::model::HybridState::Idle => HybridState::Idle(OwnedValue::from(false)), + crate::model::HybridState::Started(qr_code) => { + HybridState::Idle(value_to_owned(Value::from(qr_code))) + } + crate::model::HybridState::Connecting => HybridState::Idle(OwnedValue::from(false)), + crate::model::HybridState::Connected => HybridState::Idle(OwnedValue::from(false)), + crate::model::HybridState::Completed => HybridState::Idle(OwnedValue::from(false)), + crate::model::HybridState::UserCancelled => HybridState::Idle(OwnedValue::from(false)), + crate::model::HybridState::Failed => HybridState::Idle(OwnedValue::from(false)), + } + } +} impl TryFrom for crate::model::HybridState { type Error = zvariant::Error; fn try_from(value: HybridState) -> std::result::Result { @@ -311,6 +365,43 @@ impl TryFrom> for HybridState { } } +impl From for Value<'_> { + fn from(value: HybridState) -> Self { + let mut fields = HashMap::new(); + match value { + HybridState::Idle(owned_value) => { + fields.insert("type", value_to_owned(Value::from("IDLE"))); + fields.insert("value", owned_value); + } + HybridState::Started(owned_value) => { + fields.insert("type", value_to_owned(Value::from("STARTED"))); + fields.insert("value", owned_value); + } + HybridState::Connecting(owned_value) => { + fields.insert("type", value_to_owned(Value::from("CONNECTING"))); + fields.insert("value", owned_value); + } + HybridState::Connected(owned_value) => { + fields.insert("type", value_to_owned(Value::from("CONNECTED"))); + fields.insert("value", owned_value); + } + HybridState::Completed(owned_value) => { + fields.insert("type", value_to_owned(Value::from("COMPLETED"))); + fields.insert("value", owned_value); + } + HybridState::UserCancelled(owned_value) => { + fields.insert("type", value_to_owned(Value::from("USER_CANCELLED"))); + fields.insert("value", owned_value); + } + HybridState::Failed(owned_value) => { + fields.insert("type", value_to_owned(Value::from("FAILED"))); + fields.insert("value", owned_value); + } + } + Value::from(fields) + } +} + #[derive(Serialize, Deserialize, Type)] pub enum ServiceError { /// Some unknown error with the authenticator occurred. @@ -424,6 +515,49 @@ impl TryFrom for crate::model::UsbState { } } +impl From for UsbState { + fn from(value: crate::model::UsbState) -> Self { + match value { + crate::model::UsbState::Idle => UsbState::Idle(OwnedValue::from(false)), + crate::model::UsbState::Waiting => UsbState::Waiting(OwnedValue::from(false)), + crate::model::UsbState::SelectingDevice => { + UsbState::SelectingDevice(OwnedValue::from(false)) + } + crate::model::UsbState::Connected => UsbState::Connected(OwnedValue::from(false)), + crate::model::UsbState::NeedsPin { attempts_left } => { + let num = match attempts_left { + Some(num) => num as i32, + None => -1, + }; + UsbState::NeedsPin(OwnedValue::from(num)) + } + crate::model::UsbState::NeedsUserVerification { attempts_left } => { + let num = match attempts_left { + Some(num) => num as i32, + None => -1, + }; + UsbState::NeedsPin(OwnedValue::from(num)) + } + crate::model::UsbState::NeedsUserPresence => { + UsbState::NeedsUserPresence(OwnedValue::from(false)) + } + crate::model::UsbState::SelectCredential { creds } => { + let creds: Vec = creds.into_iter().map(Credential::from).collect(); + let value = Value::new(creds) + .try_to_owned() + .expect("All non-file descriptors to convert to OwnedValue successfully"); + UsbState::SelectCredential(value) + } + crate::model::UsbState::Completed => UsbState::Completed(OwnedValue::from(false)), + crate::model::UsbState::Failed(error) => UsbState::Failed( + Value::<'_>::from(error.to_string()) + .try_to_owned() + .expect("non-file descriptor value to convert"), + ), + } + } +} + impl TryFrom> for UsbState { type Error = zvariant::Error; fn try_from(value: Value<'_>) -> std::result::Result { @@ -433,3 +567,113 @@ impl TryFrom> for UsbState { Ok(obj) } } + +impl From for Value<'_> { + fn from(value: UsbState) -> Self { + let mut fields = HashMap::new(); + match value { + UsbState::Idle(owned_value) => { + fields.insert( + "type", + Value::from("IDLE") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::Waiting(owned_value) => { + fields.insert( + "type", + Value::from("WAITING") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::SelectingDevice(owned_value) => { + fields.insert( + "type", + Value::from("SELECTING_DEVICE") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::Connected(owned_value) => { + fields.insert( + "type", + Value::from("CONNECTED") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::NeedsPin(owned_value) => { + fields.insert( + "type", + Value::from("NEEDS_PIN") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::NeedsUserVerification(owned_value) => { + fields.insert( + "type", + Value::from("NEEDS_USER_VERIFICATION") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::NeedsUserPresence(owned_value) => { + fields.insert( + "type", + Value::from("NEEDS_USER_PRESENCE") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::SelectCredential(owned_value) => { + fields.insert( + "type", + Value::from("SELECT_CREDENTIAL") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::Completed(owned_value) => { + fields.insert( + "type", + Value::from("COMPLETED") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + UsbState::Failed(owned_value) => { + fields.insert( + "type", + Value::from("FAILED") + .try_to_owned() + .expect("non-file descriptor fields to succeed"), + ); + fields.insert("value", owned_value); + } + }; + Value::from(fields) + } +} + +#[derive(Serialize, Deserialize, Type)] +pub struct ViewRequest { + pub operation: Operation, +} + +fn value_to_owned(value: Value<'_>) -> OwnedValue { + value + .try_to_owned() + .expect("non-file descriptor values to succeed") +} diff --git a/creds-ui/Cargo.lock b/creds-ui/Cargo.lock index 8d2a7d47..b908a54a 100644 --- a/creds-ui/Cargo.lock +++ b/creds-ui/Cargo.lock @@ -783,6 +783,7 @@ dependencies = [ "qrcode", "serde", "tracing", + "tracing-subscriber", "zbus", ] @@ -2134,6 +2135,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2321,6 +2332,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "p256" version = "0.13.2" @@ -3000,6 +3017,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3214,6 +3240,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -3390,6 +3425,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -3468,6 +3529,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "value-bag" version = "1.11.1" diff --git a/creds-ui/Cargo.toml b/creds-ui/Cargo.toml index 23cb3ace..2df26d80 100644 --- a/creds-ui/Cargo.toml +++ b/creds-ui/Cargo.toml @@ -12,4 +12,5 @@ gtk = { version = "0.9.6", package = "gtk4", features = ["v4_6"] } qrcode = "0.14.1" serde = { version = "1.0.219", features = ["derive"] } tracing = "0.1.41" +tracing-subscriber = "0.3.19" zbus = "5.9.0" diff --git a/creds-ui/src/client.rs b/creds-ui/src/client.rs new file mode 100644 index 00000000..a225cc0a --- /dev/null +++ b/creds-ui/src/client.rs @@ -0,0 +1,100 @@ +use async_std::stream::Stream; +use creds_lib::client::CredentialServiceClient; +use futures_lite::StreamExt; +use zbus::{Connection, zvariant}; + +use crate::dbus::InternalServiceProxy; + +pub struct DbusCredentialClient { + conn: Connection, +} + +impl DbusCredentialClient { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + async fn proxy(&self) -> std::result::Result { + InternalServiceProxy::new(&self.conn) + .await + .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) + } +} + +impl CredentialServiceClient for DbusCredentialClient { + async fn get_available_public_key_devices( + &self, + ) -> std::result::Result, ()> { + let dbus_devices = self + .proxy() + .await? + .get_available_public_key_devices() + .await + .map_err(|_| ())?; + dbus_devices.into_iter().map(|d| d.try_into()).collect() + } + + async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { + self.proxy() + .await? + .get_hybrid_credential() + .await + .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) + .map_err(|_| ()) + } + + async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { + self.proxy() + .await? + .get_hybrid_credential() + .await + .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) + .map_err(|_| ()) + } + + async fn initiate_event_stream( + &mut self, + ) -> std::result::Result< + std::pin::Pin + Send + 'static>>, + (), + > { + let stream = self + .proxy() + .await? + .receive_state_changed() + .await + .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? + .filter_map(|msg| { + msg.args() + .and_then(|args| { + args.update + .try_into() + .map_err(|err: zvariant::Error| err.into()) + }) + .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) + .ok() + }) + .boxed(); + self.proxy() + .await? + .initiate_event_stream() + .await + .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) + .and_then(|_| Ok(stream)) + } + + async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { + self.proxy() + .await? + .enter_client_pin(pin) + .await + .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) + } + + async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { + self.proxy() + .await? + .select_credential(credential_id) + .await + .map_err(|err| tracing::error!("Failed to select credential: {err}")) + } +} diff --git a/creds-ui/src/dbus.rs b/creds-ui/src/dbus.rs index 34259c53..c755e75f 100644 --- a/creds-ui/src/dbus.rs +++ b/creds-ui/src/dbus.rs @@ -4,6 +4,7 @@ use zbus::{fdo, interface, proxy}; #[proxy( gen_blocking = false, + interface = "xyz.iinuwa.credentials.CredentialManagerInternal1", default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", default_service = "xyz.iinuwa.credentials.CredentialManagerInternal" )] @@ -32,6 +33,12 @@ pub struct UiControlService { /// These methods are called by the credential service to control the UI. #[interface(name = "xyz.iinuwa.credentials.UiControl1")] impl UiControlService { - fn launch_ui(&self, request: creds_lib::server::ViewRequest) {} + async fn launch_ui(&self, request: creds_lib::server::ViewRequest) -> fdo::Result<()> { + tracing::debug!("Received UI launch request"); + self.request_tx + .send(request) + .await + .map_err(|_| fdo::Error::Failed("UI failed to launch".to_string())) + } // fn send_state_changed(&self, event: BackgroundEvent) {} } diff --git a/creds-ui/src/main.rs b/creds-ui/src/main.rs index a05493b5..9fe6ad34 100644 --- a/creds-ui/src/main.rs +++ b/creds-ui/src/main.rs @@ -1,3 +1,4 @@ +mod client; #[rustfmt::skip] mod config; mod dbus; @@ -5,17 +6,11 @@ mod gui; use std::error::Error; -use async_std::{ - channel::{Receiver, Sender}, - stream::Stream, -}; -use creds_lib::client::CredentialServiceClient; -use futures_lite::StreamExt; -use zbus::{Connection, zvariant}; - -use crate::dbus::{InternalServiceProxy, UiControlService}; +use crate::{client::DbusCredentialClient, dbus::UiControlService}; fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + tracing::debug!("Starting credentials UI service"); async_std::task::block_on(run()) } @@ -26,10 +21,10 @@ async fn run() -> Result<(), Box> { // executing the credential flow. let conn = zbus::connection::Builder::session()?.build().await?; let cred_client = DbusCredentialClient::new(conn); - let handle = gui::start_gui_thread(request_rx, cred_client)?; + let _handle = gui::start_gui_thread(request_rx, cred_client)?; println!(" ✅"); - handle.join(); + print!("Starting UI Control listener...\t"); let interface = UiControlService { request_tx }; let path = "/xyz/iinuwa/credentials/UiControl"; let service = "xyz.iinuwa.credentials.UiControl"; @@ -38,103 +33,13 @@ async fn run() -> Result<(), Box> { .serve_at(path, interface)? .build() .await?; + println!(" ✅"); loop { std::future::pending::<()>().await; } #[allow(unreachable_code)] - Ok(()) -} - -pub struct DbusCredentialClient { - conn: Connection, -} - -impl DbusCredentialClient { - pub fn new(conn: Connection) -> Self { - Self { conn } - } - async fn proxy(&self) -> std::result::Result { - InternalServiceProxy::new(&self.conn) - .await - .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) - } -} - -impl CredentialServiceClient for DbusCredentialClient { - async fn get_available_public_key_devices( - &self, - ) -> std::result::Result, ()> { - let dbus_devices = self - .proxy() - .await? - .get_available_public_key_devices() - .await - .map_err(|_| ())?; - dbus_devices.into_iter().map(|d| d.try_into()).collect() - } - - async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { - self.proxy() - .await? - .get_hybrid_credential() - .await - .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) - .map_err(|_| ()) - } - - async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { - self.proxy() - .await? - .get_hybrid_credential() - .await - .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) - .map_err(|_| ()) - } - - async fn initiate_event_stream( - &mut self, - ) -> std::result::Result< - std::pin::Pin + Send + 'static>>, - (), - > { - let stream = self - .proxy() - .await? - .receive_state_changed() - .await - .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? - .filter_map(|msg| { - msg.args() - .and_then(|args| { - args.update - .try_into() - .map_err(|err: zvariant::Error| err.into()) - }) - .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) - .ok() - }) - .boxed(); - self.proxy() - .await? - .initiate_event_stream() - .await - .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) - .and_then(|_| Ok(stream)) - } - - async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { - self.proxy() - .await? - .enter_client_pin(pin) - .await - .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) - } - - async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { - self.proxy() - .await? - .select_credential(credential_id) - .await - .map_err(|err| tracing::error!("Failed to select credential: {err}")) + { + _ = _handle.join(); + Ok(()) } } diff --git a/credsd/src/credential_service/mod.rs b/credsd/src/credential_service/mod.rs index 21bdf3a3..03bb7a3b 100644 --- a/credsd/src/credential_service/mod.rs +++ b/credsd/src/credential_service/mod.rs @@ -14,10 +14,12 @@ use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, }; +use tokio::sync::Mutex as AsyncMutex; use creds_lib::{ client::CredentialServiceClient, - model::{CredentialRequest, CredentialResponse, Device, Transport}, + model::{CredentialRequest, CredentialResponse, Device, Operation, Transport}, + server::{CreatePublicKeyCredentialRequest, ViewRequest}, }; use crate::credential_service::{hybrid::HybridEvent, usb::UsbEvent}; @@ -25,12 +27,12 @@ use crate::credential_service::{hybrid::HybridEvent, usb::UsbEvent}; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; use usb::{UsbHandler, UsbStateInternal}; pub use { - server::{CredentialManagementClient, InProcessServer}, + server::{CredentialManagementClient, InProcessServer, UiController}, usb::UsbState, }; #[derive(Debug)] -pub struct CredentialService { +pub struct CredentialService { devices: Vec, cred_request: Mutex>, @@ -39,14 +41,14 @@ pub struct CredentialService { hybrid_handler: H, usb_handler: U, + + ui_control_client: UC, } -impl CredentialService -where -// ::Stream: Unpin + Send + Sized + 'static, -// ::Stream: Unpin + Send + Sized + 'static, +impl + CredentialService { - pub fn new(hybrid_handler: H, usb_handler: U) -> Self { + pub fn new(hybrid_handler: H, usb_handler: U, ui_control_client: UC) -> Self { let devices = vec![ Device { id: String::from("0"), @@ -65,24 +67,49 @@ where hybrid_handler, usb_handler, + + ui_control_client, } } - 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 { + pub async fn init_request(&self, request: &CredentialRequest) -> Result<(), String> { + { + let mut cred_request = self.cred_request.lock().unwrap(); + if cred_request.is_some() { + return Err("Already a request in progress.".to_string()); + } + _ = cred_request.insert(request.clone()); - Ok(()) + } + let operation = match &request { + CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, + CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, + }; + let view_request = ViewRequest { operation }; + + let launch_ui_response = self + .ui_control_client + .launch_ui(view_request) + .await + .map_err(|err| err.to_string()); + match launch_ui_response { + Ok(()) => Ok(()), + Err(err) => { + tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); + let mut cred_request = self.cred_request.lock().unwrap(); + _ = cred_request.take(); + Err(err) + } } } - async fn get_available_public_key_devices(&self) -> Result, ()> { + pub async fn get_available_public_key_devices(&self) -> Result, ()> { Ok(self.devices.to_owned()) } - fn get_hybrid_credential(&self) -> Pin + Send + 'static>> { + pub fn get_hybrid_credential( + &self, + ) -> Pin + Send + 'static>> { let guard = self.cred_request.lock().unwrap(); let cred_request = guard.clone().unwrap(); let stream = self.hybrid_handler.start(&cred_request); @@ -93,7 +120,7 @@ where }) } - fn get_usb_credential(&self) -> Pin + Send + 'static>> { + pub fn get_usb_credential(&self) -> Pin + Send + 'static>> { let guard = self.cred_request.lock().unwrap(); let cred_request = guard.clone().unwrap(); let stream = self.usb_handler.start(&cred_request); diff --git a/credsd/src/credential_service/server.rs b/credsd/src/credential_service/server.rs index a371d1b2..7ab9e2c5 100644 --- a/credsd/src/credential_service/server.rs +++ b/credsd/src/credential_service/server.rs @@ -1,9 +1,11 @@ +use std::error::Error; use std::fmt::Debug; use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; use creds_lib::client::CredentialServiceClient; +use creds_lib::server::ViewRequest; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; @@ -104,7 +106,7 @@ pub trait CredentialManagementClient { fn get_available_public_key_devices( &self, - ) -> impl Future, ()>> + Send; + ) -> impl Future, Box>> + Send; fn get_hybrid_credential(&mut self) -> impl Future> + Send; fn get_usb_credential(&mut self) -> impl Future> + Send; @@ -121,24 +123,28 @@ pub trait CredentialManagementClient { } #[derive(Debug)] -pub struct InProcessManager +pub struct InProcessManager where H: HybridHandler + Debug + Send + Sync, U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, { tx: mpsc::Sender<( InProcessServerRequest, oneshot::Sender, )>, - svc: Arc>>, + svc: Arc>>, bg_event_tx: Option>, usb_pin_tx: Arc>>>, usb_event_forwarder_task: Arc>>, hybrid_event_forwarder_task: Arc>>, } -impl Clone - for InProcessManager +impl< + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, + > Clone for InProcessManager { fn clone(&self) -> Self { Self { @@ -152,8 +158,11 @@ impl - InProcessManager +impl< + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, + > InProcessManager { async fn send(&self, request: ManagementRequest) -> Result { let (response_tx, response_rx) = oneshot::channel(); @@ -175,11 +184,14 @@ impl - CredentialManagementClient for InProcessManager +impl< + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, + > CredentialManagementClient for InProcessManager { async fn init_request(&self, cred_request: CredentialRequest) -> Result<(), String> { - self.svc.lock().await.init_request(&cred_request) + self.svc.lock().await.init_request(&cred_request).await } async fn complete_auth(&self) -> Result { @@ -190,12 +202,15 @@ impl Result, ()> { - self.svc + async fn get_available_public_key_devices(&self) -> Result, Box> { + let devices = self + .svc .lock() .await .get_available_public_key_devices() .await + .map_err(|_| "Failed to get public key devices".to_string())?; + Ok(devices) } async fn get_hybrid_credential(&mut self) -> Result<(), ()> { @@ -296,8 +311,11 @@ impl Drop - for InProcessManager +impl< + H: HybridHandler + Debug + Send + Sync, + U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, + > Drop for InProcessManager { fn drop(&mut self) { if let Some(task) = self.usb_event_forwarder_task.lock().unwrap().take() { @@ -429,24 +447,28 @@ impl CredentialServiceClient for ArcInProcessClient { } #[derive(Debug)] -pub struct InProcessServer +pub struct InProcessServer where H: HybridHandler + Debug + Send + Sync, U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, { rx: mpsc::Receiver<( InProcessServerRequest, oneshot::Sender, )>, - mgr: InProcessManager, + mgr: InProcessManager, } -impl InProcessServer +impl InProcessServer where H: HybridHandler + Debug + Send + Sync, U: UsbHandler + Debug + Send + Sync, + UC: UiController + Debug + Send + Sync, { - pub fn new(svc: CredentialService) -> (Self, InProcessManager, InProcessClient) { + pub fn new( + svc: CredentialService, + ) -> (Self, InProcessManager, InProcessClient) { let (tx, rx) = mpsc::channel(256); let svc_arc = Arc::new(AsyncMutex::new(svc)); @@ -517,3 +539,11 @@ where } } } + +/// Used by the credential service to control the UI. +pub trait UiController { + fn launch_ui( + &self, + request: ViewRequest, + ) -> impl Future>> + Send; +} diff --git a/credsd/src/dbus.rs b/credsd/src/dbus.rs index 133d101a..4d261b66 100644 --- a/credsd/src/dbus.rs +++ b/credsd/src/dbus.rs @@ -31,18 +31,20 @@ mod model; +use std::pin::Pin; +use std::{collections::VecDeque, error::Error, fmt::Debug, sync::Arc}; + use creds_lib::server::{CreateCredentialRequest, ViewRequest}; -use futures_lite::StreamExt; -use std::collections::VecDeque; -use std::error::Error; -use std::sync::Arc; +use futures_lite::{Stream, StreamExt}; +use tokio::sync::mpsc::Sender; use tokio::sync::Mutex as AsyncMutex; -use zbus::object_server::SignalEmitter; -use zbus::proxy; +use tokio::task::AbortHandle; +use zbus::object_server::{InterfaceRef, SignalEmitter}; use zbus::{ connection::{self, Connection}, fdo, interface, Result, }; +use zbus::{proxy, ObjectServer}; use creds_lib::{ client::CredentialServiceClient, @@ -60,16 +62,15 @@ use self::model::{ create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, }; -use crate::credential_service::CredentialManagementClient; - -// TODO: This is a workaround for testing credential_service. Refactor so that -// these private structs don't need to be exported. -// pub use self::model::{CreateCredentialRequest, CreatePublicKeyCredentialRequest}; +use crate::credential_service::hybrid::{HybridHandler, HybridState}; +use crate::credential_service::usb::UsbHandler; +use crate::credential_service::{ + CredentialManagementClient, CredentialService, UiController, UsbState, +}; pub(crate) async fn start_service( service_name: &str, path: &str, - // gui_tx: Sender, manager_client: C, ) -> Result { let lock = Arc::new(AsyncMutex::new(())); @@ -235,8 +236,37 @@ impl CredentialManager } } -struct InternalService { +pub async fn start_internal_service< + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +>( + service_name: &str, + path: &str, + credential_service: CredentialService, +) -> Result { + connection::Builder::session()? + .name(service_name)? + .serve_at( + path, + InternalService { + signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), + svc: Arc::new(AsyncMutex::new(credential_service)), + usb_pin_tx: Arc::new(AsyncMutex::new(None)), + usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), + hybrid_event_forwarder_task: Arc::new(AsyncMutex::new(None)), + }, + )? + .build() + .await +} + +pub struct InternalService { signal_state: Arc>, + svc: Arc>>, + usb_pin_tx: Arc>>>, + usb_event_forwarder_task: Arc>>, + hybrid_event_forwarder_task: Arc>>, } /// The following methods are for communication between the [trusted] @@ -250,7 +280,12 @@ struct InternalService { default_service = "xyz.iinuwa.credentials.CredentialManagerInternal", ) )] -impl InternalService { +impl InternalService +where + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +{ async fn initiate_event_stream( &self, #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, @@ -270,77 +305,260 @@ impl InternalService { } async fn get_available_public_key_devices(&self) -> fdo::Result> { - todo!() + let devices = self + .svc + .lock() + .await + .get_available_public_key_devices() + .await + .map_err(|_| { + fdo::Error::Failed("Failed to get retrieve available devices".to_string()) + })?; + Ok(devices.into_iter().map(Device::from).collect()) } - async fn get_hybrid_credential(&self) -> fdo::Result<()> { - todo!() + async fn get_hybrid_credential( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + ) -> fdo::Result<()> { + let svc = self.svc.lock().await; + let mut stream = svc.get_hybrid_credential(); + let signal_state = self.signal_state.clone(); + let object_server = object_server.clone(); + let task = tokio::spawn(async move { + let interface: Result>> = object_server + .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") + .await; + + let emitter = match interface { + Ok(ref i) => i.signal_emitter(), + Err(err) => { + tracing::error!("Failed to get connection to D-Bus to send signals: {err}"); + return; + } + }; + while let Some(state) = stream.next().await { + let event = + creds_lib::model::BackgroundEvent::HybridQrStateChanged(state.clone().into()) + .try_into(); + match event { + Err(err) => { + tracing::error!("Failed to serialize state update: {err}"); + break; + } + Ok(event) => match send_state_update(&emitter, &signal_state, event).await { + Ok(_) => {} + Err(err) => { + tracing::error!("Failed to send state update to UI: {err}"); + break; + } + }, + } + match state { + HybridState::Completed | HybridState::Failed => { + break; + } + _ => {} + }; + } + }) + .abort_handle(); + if let Some(prev_task) = self.hybrid_event_forwarder_task.lock().await.replace(task) { + prev_task.abort(); + } + Ok(()) } - async fn get_usb_credential(&self) -> fdo::Result<()> { - todo!() + async fn get_usb_credential( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + ) -> fdo::Result<()> { + let mut stream = self.svc.lock().await.get_usb_credential(); + let usb_pin_tx = self.usb_pin_tx.clone(); + let signal_state = self.signal_state.clone(); + let object_server = object_server.clone(); + let task = tokio::spawn(async move { + let interface: Result>> = object_server + .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") + .await; + + let emitter = match interface { + Ok(ref i) => i.signal_emitter(), + Err(err) => { + tracing::error!("Failed to get connection to D-Bus to send signals: {err}"); + return; + } + }; + while let Some(state) = stream.next().await { + match creds_lib::model::BackgroundEvent::UsbStateChanged((&state).into()).try_into() + { + Err(err) => { + tracing::error!("Failed to serialize state update: {err}"); + break; + } + Ok(event) => match send_state_update(&emitter, &signal_state, event).await { + Ok(_) => {} + Err(err) => { + tracing::error!("Failed to send state update to UI: {err}"); + break; + } + }, + }; + match state { + UsbState::NeedsPin { pin_tx, .. } => { + let mut usb_pin_tx = usb_pin_tx.lock().await; + let _ = usb_pin_tx.insert(pin_tx); + } + UsbState::Completed | UsbState::Failed(_) => { + break; + } + _ => {} + }; + } + }) + .abort_handle(); + if let Some(prev_task) = self.usb_event_forwarder_task.lock().await.replace(task) { + prev_task.abort(); + } + Ok(()) } async fn select_device(&self, device_id: String) -> fdo::Result<()> { todo!() } + async fn enter_client_pin(&self, pin: String) -> fdo::Result<()> { - todo!() + if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { + pin_tx.send(pin).await.unwrap(); + } + Ok(()) } + async fn select_credential(&self, credential_id: String) -> fdo::Result<()> { todo!() } - async fn send_state_update( - &self, - #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, - update: BackgroundEvent, - ) -> fdo::Result<()> { - let mut signal_state = self.signal_state.lock().await; - match *signal_state { - SignalState::Idle => { - let pending = VecDeque::from([update]); - *signal_state = SignalState::Pending(pending); - } - SignalState::Pending(ref mut pending) => { - pending.push_back(update); - } - SignalState::Active => { - emitter.state_changed(update).await?; - } - }; - Ok(()) - } - #[zbus(signal)] async fn state_changed( emitter: &SignalEmitter<'_>, update: BackgroundEvent, ) -> zbus::Result<()>; } +async fn send_state_update( + emitter: &SignalEmitter<'_>, + signal_state: &Arc>, + update: BackgroundEvent, +) -> fdo::Result<()> { + let mut signal_state = signal_state.lock().await; + match *signal_state { + SignalState::Idle => { + let pending = VecDeque::from([update]); + *signal_state = SignalState::Pending(pending); + } + SignalState::Pending(ref mut pending) => { + pending.push_back(update); + } + SignalState::Active => { + emitter.state_changed(update).await?; + } + }; + Ok(()) +} + +pub struct CredentialControlServiceClient { + conn: Connection, +} + +impl CredentialControlServiceClient { + async fn proxy(&self) -> Result { + InternalServiceProxy::new(&self.conn).await + } +} + +impl CredentialManagementClient for CredentialControlServiceClient { + fn init_request( + &self, + cred_request: CredentialRequest, + ) -> impl std::prelude::rust_2024::Future> + Send { + todo!() + } + + fn complete_auth( + &self, + ) -> impl std::prelude::rust_2024::Future> + + Send { + todo!() + } + + async fn get_available_public_key_devices( + &self, + ) -> std::result::Result, Box> { + let devices: std::result::Result, String> = self + .proxy() + .await? + .get_available_public_key_devices() + .await? + .into_iter() + .map(|d| d.try_into().map_err(|_| "Failed".to_string())) + .collect(); + Ok(devices?) + } + + async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { + todo!() + } + + async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { + todo!() + } + + async fn initiate_event_stream( + &mut self, + ) -> std::result::Result< + Pin + Send + 'static>>, + (), + > { + todo!() + } + + async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { + if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { + pin_tx.send(pin).await.unwrap(); + } + Ok(()) + } + + async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { + todo!() + } +} /// These methods are called by the credential service to control the UI. #[proxy( gen_blocking = false, - default_path = "/xyz/iinuwa/credentials/UiControl", - default_service = "xyz.iinuwa.credentials.UiControl" + interface = "xyz.iinuwa.credentials.UiControl1", + default_service = "xyz.iinuwa.credentials.UiControl", + default_path = "/xyz/iinuwa/credentials/UiControl" )] +// The #[proxy] macro renames this type to this creates a type UiControlServiceClientProxy trait UiControlServiceClient { fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; } -trait UiControlServiceClient { - async fn launch_ui(&self, request: ViewRequest) -> std::result::Result<(), Box>; -} -struct UiControlServiceImpl { +#[derive(Debug)] +pub struct UiControlServiceClient { conn: Connection, } -impl UiControlServiceImpl { +impl UiControlServiceClient { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + async fn proxy(&self) -> std::result::Result { UiControlServiceClientProxy::new(&self.conn).await } } -impl UiControlServiceClient for UiControlServiceImpl { +impl UiController for UiControlServiceClient { async fn launch_ui(&self, request: ViewRequest) -> std::result::Result<(), Box> { self.proxy() .await? diff --git a/credsd/src/main.rs b/credsd/src/main.rs index dd75101d..d98e51a1 100644 --- a/credsd/src/main.rs +++ b/credsd/src/main.rs @@ -8,8 +8,11 @@ mod webauthn; use std::{error::Error, sync::Arc}; -use crate::credential_service::{ - hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, InProcessServer, +use crate::{ + credential_service::{ + hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, InProcessServer, + }, + dbus::UiControlServiceClient, }; #[tokio::main] @@ -25,20 +28,30 @@ async fn main() { } async fn run() -> Result<(), Box> { - let credential_service = - CredentialService::new(InternalHybridHandler::new(), InProcessUsbHandler {}); - print!("Starting credential service...\t"); - let (mut cred_server, cred_mgr, cred_client) = InProcessServer::new(credential_service); - tokio::spawn(async move { - cred_server.run().await; - }); + print!("Connecting to D-Bus as client...\t"); + let dbus_client_conn = zbus::connection::Builder::session()?.build().await?; println!(" ✅"); - print!("Starting D-Bus service..."); + print!("Starting D-Bus public client service..."); let service_name = "xyz.iinuwa.credentials.CredentialManagerUi"; let path = "/xyz/iinuwa/credentials/CredentialManagerUi"; - let _conn = dbus::start_service(service_name, path, /* dbus_to_gui_tx, */ cred_mgr).await?; + let _conn = dbus::start_service(service_name, path, cred_mgr).await?; println!(" ✅"); + + print!("Starting D-Bus UI -> Credential control service..."); + let ui_controller = UiControlServiceClient::new(dbus_client_conn); + let credential_service = CredentialService::new( + InternalHybridHandler::new(), + InProcessUsbHandler {}, + ui_controller, + ); + let internal_service_name = "xyz.iinuwa.credentials.CredentialManagerInternal"; + let internal_path = "/xyz/iinuwa/credentials/CredentialManagerInternal"; + let _internal_service = + dbus::start_internal_service(internal_service_name, internal_path, credential_service) + .await?; + println!(" ✅"); + println!("Waiting for messages..."); loop { // wait forever, handle D-Bus in the background From 84cd7c8133d3070c0641c726aa89598b9be8a52e Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 2 Aug 2025 08:18:34 -0500 Subject: [PATCH 21/38] Rename main service interface --- contrib/xyz.iinuwa.credentials.CredentialManager.xml | 2 +- credsd/src/dbus.rs | 2 +- credsd/src/main.rs | 7 ++++--- demo_client/main.py | 11 ++++++----- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/contrib/xyz.iinuwa.credentials.CredentialManager.xml b/contrib/xyz.iinuwa.credentials.CredentialManager.xml index 056fcd89..f8182445 100644 --- a/contrib/xyz.iinuwa.credentials.CredentialManager.xml +++ b/contrib/xyz.iinuwa.credentials.CredentialManager.xml @@ -7,7 +7,7 @@ - + diff --git a/credsd/src/dbus.rs b/credsd/src/dbus.rs index 4d261b66..2c729707 100644 --- a/credsd/src/dbus.rs +++ b/credsd/src/dbus.rs @@ -104,7 +104,7 @@ struct CredentialManager { } /// These are public methods that can be called by arbitrary clients to begin a credential flow. -#[interface(name = "xyz.iinuwa.credentials.CredentialManagerUi1")] +#[interface(name = "xyz.iinuwa.credentials.Credentials1")] impl CredentialManager { async fn create_credential( &self, diff --git a/credsd/src/main.rs b/credsd/src/main.rs index d98e51a1..1d62a453 100644 --- a/credsd/src/main.rs +++ b/credsd/src/main.rs @@ -12,7 +12,7 @@ use crate::{ credential_service::{ hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, InProcessServer, }, - dbus::UiControlServiceClient, + dbus::{CredentialControlServiceClient, UiControlServiceClient}, }; #[tokio::main] @@ -33,8 +33,9 @@ async fn run() -> Result<(), Box> { println!(" ✅"); print!("Starting D-Bus public client service..."); - let service_name = "xyz.iinuwa.credentials.CredentialManagerUi"; - let path = "/xyz/iinuwa/credentials/CredentialManagerUi"; + let service_name = "xyz.iinuwa.credentials.Credentials"; + let path = "/xyz/iinuwa/credentials/Credentials"; + let cred_mgr = CredentialControlServiceClient::new(dbus_client_conn.clone()); let _conn = dbus::start_service(service_name, path, cred_mgr).await?; println!(" ✅"); diff --git a/demo_client/main.py b/demo_client/main.py index aabda2ec..e2d439fd 100755 --- a/demo_client/main.py +++ b/demo_client/main.py @@ -20,12 +20,13 @@ async def run(cmd): with open('../contrib/xyz.iinuwa.credentials.CredentialManager.xml', 'r') as f: introspection = f.read() - proxy_object = bus.get_proxy_object('xyz.iinuwa.credentials.CredentialManagerUi', - '/xyz/iinuwa/credentials/CredentialManagerUi', - introspection) + proxy_object = bus.get_proxy_object( + "xyz.iinuwa.credentials.Credentials", + "/xyz/iinuwa/credentials/Credentials", + introspection, + ) - interface = proxy_object.get_interface( - 'xyz.iinuwa.credentials.CredentialManagerUi1') + interface = proxy_object.get_interface("xyz.iinuwa.credentials.Credentials1") rp_id = "example.com" origin = "https://example.com" From faeb80eaca370b38554a7c6586c3332425a95f8e Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 2 Aug 2025 08:18:34 -0500 Subject: [PATCH 22/38] Apply Python format --- demo_client/main.py | 242 +++++++++++++++++++++++--------------------- 1 file changed, 124 insertions(+), 118 deletions(-) diff --git a/demo_client/main.py b/demo_client/main.py index e2d439fd..bfdad37c 100755 --- a/demo_client/main.py +++ b/demo_client/main.py @@ -14,10 +14,11 @@ import util import webauthn + async def run(cmd): bus = await MessageBus().connect() - with open('../contrib/xyz.iinuwa.credentials.CredentialManager.xml', 'r') as f: + with open("../contrib/xyz.iinuwa.credentials.CredentialManager.xml", "r") as f: introspection = f.read() proxy_object = bus.get_proxy_object( @@ -34,8 +35,10 @@ async def run(cmd): user_handle = b"123abdsacddw" username = "user@example.com" - if cmd == 'create': - auth_data = await create_passkey(interface, origin, top_origin, rp_id, user_handle, username) + if cmd == "create": + auth_data = await create_passkey( + interface, origin, top_origin, rp_id, user_handle, username + ) user_data = { "id": 1, "name": username, @@ -43,17 +46,19 @@ async def run(cmd): "cred_id": util.b64_encode(auth_data.cred_id), "pub_key": util.b64_encode(auth_data.pub_key_bytes), "sign_count": auth_data.sign_count, - "backup_eligible": auth_data.has_flag('BE'), - "backup_state": auth_data.has_flag('BS'), - "uv_initialized": auth_data.has_flag('UV'), + "backup_eligible": auth_data.has_flag("BE"), + "backup_state": auth_data.has_flag("BS"), + "uv_initialized": auth_data.has_flag("UV"), } print("New credential data:") print(json.dumps(user_data)) - json.dump(user_data, open('./user.json', 'w')) - elif cmd == 'get': - user_data = json.load(open('./user.json', 'r')) - cred_id = util.b64_decode(user_data['cred_id']) - auth_data = await get_passkey(interface, origin, top_origin, rp_id, cred_id, user_data) + json.dump(user_data, open("./user.json", "w")) + elif cmd == "get": + user_data = json.load(open("./user.json", "r")) + cred_id = util.b64_decode(user_data["cred_id"]) + auth_data = await get_passkey( + interface, origin, top_origin, rp_id, cred_id, user_data + ) print(auth_data) else: print(f"unknown cmd: {cmd}") @@ -67,12 +72,17 @@ async def run(cmd): async def create_password(interface): password_req = { - "type": Variant('s', "password"), - "password": Variant("a{sv}", { - "origin": Variant('s', "xyz.iinuwa.credentials.CredentialManager:local"), - "id": Variant('s', "test@example.com"), - "password": Variant('s', "abc123"), - }) + "type": Variant("s", "password"), + "password": Variant( + "a{sv}", + { + "origin": Variant( + "s", "xyz.iinuwa.credentials.CredentialManager:local" + ), + "id": Variant("s", "test@example.com"), + "password": Variant("s", "abc123"), + }, + ), } rsp = await interface.call_create_credential(password_req) return rsp @@ -81,18 +91,21 @@ async def create_password(interface): async def get_password(interface): password_req = { "origin": Variant("s", "xyz.iinuwa.credentials.CredentialManager:local"), - "options": Variant("aa{sv}", [ - { - "type": Variant("s", "password"), - "password": Variant("a{sv}", {}), - } - ]) + "options": Variant( + "aa{sv}", + [ + { + "type": Variant("s", "password"), + "password": Variant("a{sv}", {}), + } + ], + ), } rsp = await interface.call_get_credential(password_req) - if rsp['type'].value == 'password': - cred = rsp['password'].value - id = cred['id'].value - password = cred['password'].value + if rsp["type"].value == "password": + cred = rsp["password"].value + id = cred["id"].value + password = cred["password"].value return (id, password) return None @@ -117,31 +130,37 @@ async def create_passkey(interface, origin, top_origin, rp_id, user_handle, user ], } - print(f"Sending {'same' if is_same_origin else 'cross'}-origin request for {origin} using options:") + print( + f"Sending {'same' if is_same_origin else 'cross'}-origin request for {origin} using options:" + ) pprint(options) print() req_json = json.dumps(options) req = { - "type": Variant('s', "publicKey"), - "origin": Variant('s', origin), - "is_same_origin": Variant('b', is_same_origin), - "publicKey": Variant('a{sv}', { - "request_json": Variant('s', req_json) - }) + "type": Variant("s", "publicKey"), + "origin": Variant("s", origin), + "is_same_origin": Variant("b", is_same_origin), + "publicKey": Variant("a{sv}", {"request_json": Variant("s", req_json)}), } rsp = await interface.call_create_credential(req) print("Received response") pprint(rsp) - if rsp['type'].value != 'public-key': - raise Exception(f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}") + if rsp["type"].value != "public-key": + raise Exception( + f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}" + ) - response_json = json.loads(rsp['public_key'].value['registration_response_json'].value) + response_json = json.loads( + rsp["public_key"].value["registration_response_json"].value + ) return webauthn.verify_create_response(response_json, options, origin) -async def get_passkey(interface, origin, top_origin, rp_id, cred_id, user: Optional[dict]): +async def get_passkey( + interface, origin, top_origin, rp_id, cred_id, user: Optional[dict] +): is_same_origin = origin == top_origin options = { "challenge": util.b64_encode(secrets.token_bytes(16)), @@ -151,27 +170,31 @@ async def get_passkey(interface, origin, top_origin, rp_id, cred_id, user: Optio ], } - print(f"Sending {'same' if is_same_origin else 'cross'}-origin request for {origin} using options:") + print( + f"Sending {'same' if is_same_origin else 'cross'}-origin request for {origin} using options:" + ) pprint(options) print() req_json = json.dumps(options) req = { - "type": Variant('s', "publicKey"), - "origin": Variant('s', origin), - "is_same_origin": Variant('b', is_same_origin), - "publicKey": Variant('a{sv}', { - "request_json": Variant('s', req_json) - }) + "type": Variant("s", "publicKey"), + "origin": Variant("s", origin), + "is_same_origin": Variant("b", is_same_origin), + "publicKey": Variant("a{sv}", {"request_json": Variant("s", req_json)}), } rsp = await interface.call_get_credential(req) print("Received response") pprint(rsp) - if rsp['type'].value != 'public-key': - raise Exception(f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}") + if rsp["type"].value != "public-key": + raise Exception( + f"Invalid credential type received: expected 'public-key', received {rsp['type'.value]}" + ) - response_json = json.loads(rsp['public_key'].value['authentication_response_json'].value) + response_json = json.loads( + rsp["public_key"].value["authentication_response_json"].value + ) print(user) return webauthn.verify_get_response(response_json, options, origin, user, None) @@ -189,57 +212,45 @@ def main(): if __name__ == "__main__": main() + class VerificationTests(unittest.TestCase): def test_create_credential(self): response = { - 'id': 'owBYoufBWYDUOeNB9dZs9x6GlEPiS8ziKnI_9YVq9RpkwwYsxelm66HOP2usfy-SaV8NE5nJVWDIMvS0W-x9BYtN4AmHZVY33GW2rdfLpeBruuh4jDXgYdnHtZC0IyDIKZiOTzSzyoQih8F-VLcTmqQl7SVHgf-xAh-6TxAJMccROZyIsili1OOnv3WSE7374c2Sw9At0ILaSiTmvC7MtZfnj9hhnAFMFobCJvainepVBn3HAlDo22486wkPqW2D5N00XYXK', - 'rawId': 'owBYoufBWYDUOeNB9dZs9x6GlEPiS8ziKnI_9YVq9RpkwwYsxelm66HOP2usfy-SaV8NE5nJVWDIMvS0W-x9BYtN4AmHZVY33GW2rdfLpeBruuh4jDXgYdnHtZC0IyDIKZiOTzSzyoQih8F-VLcTmqQl7SVHgf-xAh-6TxAJMccROZyIsili1OOnv3WSE7374c2Sw9At0ILaSiTmvC7MtZfnj9hhnAFMFobCJvainepVBn3HAlDo22486wkPqW2D5N00XYXK', - 'response': { - 'attestationObject': 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgIQ1ReuY8bt2QPrmsZGqphT3hwTJ4Ar2zd3RevRXelHYCIQDiSKGGo5mUqsWP43B6TgxcWby0M1ucBkwOQTS4E6Dt-mN4NWOBWQKqMIICpjCCAkygAwIBAgIUfWe3F4mJfmOVopPF8mmAKxBb0igwCgYIKoZIzj0EAwIwLTERMA8GA1UECgwIU29sb0tleXMxCzAJBgNVBAYTAkNIMQswCQYDVQQDDAJGMTAgFw0yMTA1MjMwMDUyMDBaGA8yMDcxMDUxMTAwNTIwMFowgYMxCzAJBgNVBAYTAlVTMREwDwYDVQQKDAhTb2xvS2V5czEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjE9MDsGA1UEAww0U29sbyAyIE5GQytVU0ItQSA4NjUyQUJFOUZCRDg0ODEwQTg0MEQ2RkM0NDJBOEMyQyBCMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABArSyTVT7sDxX0rom6XoIcg8qwMStGV3SjoGRNMqHBSAh2sr4EllUzA1F8yEX5XvUPN_M6DQlqEFGw18UodOjBqjgfAwge0wHQYDVR0OBBYEFBiTdxTWyNCRuzSieBflmHPSJbS1MB8GA1UdIwQYMBaAFEFrtkvvohkN5GJf_SkElrmCKbT4MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL2kuczJwa2kubmV0L2YxLzAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8vYy5zMnBraS5uZXQvcjEvMCEGCysGAQQBguUcAQEEBBIEEIZSq-n72EgQqEDW_EQqjCwwEwYLKwYBBAGC5RwCAQEEBAMCBDAwCgYIKoZIzj0EAwIDSAAwRQIgMsLnUg5Px2FehxIUNiaey8qeT1FGtlJ1s3LEUGOks-8CIQDNEv5aupDvYxn2iqWSNysv4qpdoqSMytRQ7ctfuJDWN2hhdXRoRGF0YVkBJ6N5pvbur7mlXjeMEYA04nUeaC-rny0wqxPSElWGzhlHRQAAADmGUqvp-9hIEKhA1vxEKowsAMajAFii58FZgNQ540H11mz3HoaUQ-JLzOIqcj_1hWr1GmTDBizF6Wbroc4_a6x_L5JpXw0TmclVYMgy9LRb7H0Fi03gCYdlVjfcZbat18ul4Gu66HiMNeBh2ce1kLQjIMgpmI5PNLPKhCKHwX5UtxOapCXtJUeB_7ECH7pPEAkxxxE5nIiyKWLU46e_dZITvfvhzZLD0C3QgtpKJOa8Lsy1l-eP2GGcAUwWhsIm9qKd6lUGfccCUOjbbjzrCQ-pbYPk3TRdhcqkAQEDJyAGIVggzFQIxv1GYCb7CZXbKR8VRTWiRCbceHYcsBNx-lOg9Xk', - 'clientDataJSON': 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiai1keUY4WGN3NWxZMllGSjI2MHl3ZyIsIm9yaWdpbiI6Inh5ei5paW51d2EuY3JlZGVudGlhbHMuQ3JlZGVudGlhbE1hbmFnZXI6bG9jYWwiLCJjcm9zc09yaWdpbiI6dHJ1ZX0', - 'transports': ['usb'] - } + "id": "owBYoufBWYDUOeNB9dZs9x6GlEPiS8ziKnI_9YVq9RpkwwYsxelm66HOP2usfy-SaV8NE5nJVWDIMvS0W-x9BYtN4AmHZVY33GW2rdfLpeBruuh4jDXgYdnHtZC0IyDIKZiOTzSzyoQih8F-VLcTmqQl7SVHgf-xAh-6TxAJMccROZyIsili1OOnv3WSE7374c2Sw9At0ILaSiTmvC7MtZfnj9hhnAFMFobCJvainepVBn3HAlDo22486wkPqW2D5N00XYXK", + "rawId": "owBYoufBWYDUOeNB9dZs9x6GlEPiS8ziKnI_9YVq9RpkwwYsxelm66HOP2usfy-SaV8NE5nJVWDIMvS0W-x9BYtN4AmHZVY33GW2rdfLpeBruuh4jDXgYdnHtZC0IyDIKZiOTzSzyoQih8F-VLcTmqQl7SVHgf-xAh-6TxAJMccROZyIsili1OOnv3WSE7374c2Sw9At0ILaSiTmvC7MtZfnj9hhnAFMFobCJvainepVBn3HAlDo22486wkPqW2D5N00XYXK", + "response": { + "attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIgIQ1ReuY8bt2QPrmsZGqphT3hwTJ4Ar2zd3RevRXelHYCIQDiSKGGo5mUqsWP43B6TgxcWby0M1ucBkwOQTS4E6Dt-mN4NWOBWQKqMIICpjCCAkygAwIBAgIUfWe3F4mJfmOVopPF8mmAKxBb0igwCgYIKoZIzj0EAwIwLTERMA8GA1UECgwIU29sb0tleXMxCzAJBgNVBAYTAkNIMQswCQYDVQQDDAJGMTAgFw0yMTA1MjMwMDUyMDBaGA8yMDcxMDUxMTAwNTIwMFowgYMxCzAJBgNVBAYTAlVTMREwDwYDVQQKDAhTb2xvS2V5czEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjE9MDsGA1UEAww0U29sbyAyIE5GQytVU0ItQSA4NjUyQUJFOUZCRDg0ODEwQTg0MEQ2RkM0NDJBOEMyQyBCMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABArSyTVT7sDxX0rom6XoIcg8qwMStGV3SjoGRNMqHBSAh2sr4EllUzA1F8yEX5XvUPN_M6DQlqEFGw18UodOjBqjgfAwge0wHQYDVR0OBBYEFBiTdxTWyNCRuzSieBflmHPSJbS1MB8GA1UdIwQYMBaAFEFrtkvvohkN5GJf_SkElrmCKbT4MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL2kuczJwa2kubmV0L2YxLzAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8vYy5zMnBraS5uZXQvcjEvMCEGCysGAQQBguUcAQEEBBIEEIZSq-n72EgQqEDW_EQqjCwwEwYLKwYBBAGC5RwCAQEEBAMCBDAwCgYIKoZIzj0EAwIDSAAwRQIgMsLnUg5Px2FehxIUNiaey8qeT1FGtlJ1s3LEUGOks-8CIQDNEv5aupDvYxn2iqWSNysv4qpdoqSMytRQ7ctfuJDWN2hhdXRoRGF0YVkBJ6N5pvbur7mlXjeMEYA04nUeaC-rny0wqxPSElWGzhlHRQAAADmGUqvp-9hIEKhA1vxEKowsAMajAFii58FZgNQ540H11mz3HoaUQ-JLzOIqcj_1hWr1GmTDBizF6Wbroc4_a6x_L5JpXw0TmclVYMgy9LRb7H0Fi03gCYdlVjfcZbat18ul4Gu66HiMNeBh2ce1kLQjIMgpmI5PNLPKhCKHwX5UtxOapCXtJUeB_7ECH7pPEAkxxxE5nIiyKWLU46e_dZITvfvhzZLD0C3QgtpKJOa8Lsy1l-eP2GGcAUwWhsIm9qKd6lUGfccCUOjbbjzrCQ-pbYPk3TRdhcqkAQEDJyAGIVggzFQIxv1GYCb7CZXbKR8VRTWiRCbceHYcsBNx-lOg9Xk", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiai1keUY4WGN3NWxZMllGSjI2MHl3ZyIsIm9yaWdpbiI6Inh5ei5paW51d2EuY3JlZGVudGlhbHMuQ3JlZGVudGlhbE1hbmFnZXI6bG9jYWwiLCJjcm9zc09yaWdpbiI6dHJ1ZX0", + "transports": ["usb"], + }, } - challenge = 'j-dyF8Xcw5lY2YFJ260ywg' + challenge = "j-dyF8Xcw5lY2YFJ260ywg" create_options = { - 'challenge': challenge, - 'rp': { - 'id': 'example.com' - }, - 'authenticatorSelection': { - 'userVerification': 'required' - }, - 'pubKeyCredParams': [ - { - "type": "public-key", - "alg": -8 - }, - { - "type": "public-key", - "alg": -7 - }, - { - "type": "public-key", - "alg": -257 - } - ] + "challenge": challenge, + "rp": {"id": "example.com"}, + "authenticatorSelection": {"userVerification": "required"}, + "pubKeyCredParams": [ + {"type": "public-key", "alg": -8}, + {"type": "public-key", "alg": -7}, + {"type": "public-key", "alg": -257}, + ], } - origin = "xyz.iinuwa.credentials.CredentialManager:local" + origin = "xyz.iinuwa.credentials.CredentialManager:local" auth_data = webauthn.verify_create_response(response, create_options, origin) - self.assertEqual(response['id'], util.b64_encode(auth_data.cred_id)) + self.assertEqual(response["id"], util.b64_encode(auth_data.cred_id)) def test_get_credential(self): response = { - "authenticatorAttachment":"cross-platform", - "id":"owBYojOVzZU-pjscj82gQAHvhUDTMgzQtTcQjyBpzHT-bqLwtLF2OOJDoskE18lOn2-1-SV-b7nCvn5s5Uq2KhBt1Q9kFVBUsb8jBl959BY3KWTg2rgjpN9nB5uIWTEFXfAWo0qIYGGVhXLyEbvu72Lq_W0wlccoKlxWrP349qN9OG2RTaGrgNjxTo1LqnSVc9S6D1zD7mop5KQ_9FZEjA5jABAquwFMAuO4ongyujnpoAfyAlB6UZ_JDmDFCkuN598q_LAu", - "rawId":"owBYojOVzZU-pjscj82gQAHvhUDTMgzQtTcQjyBpzHT-bqLwtLF2OOJDoskE18lOn2-1-SV-b7nCvn5s5Uq2KhBt1Q9kFVBUsb8jBl959BY3KWTg2rgjpN9nB5uIWTEFXfAWo0qIYGGVhXLyEbvu72Lq_W0wlccoKlxWrP349qN9OG2RTaGrgNjxTo1LqnSVc9S6D1zD7mop5KQ_9FZEjA5jABAquwFMAuO4ongyujnpoAfyAlB6UZ_JDmDFCkuN598q_LAu", + "authenticatorAttachment": "cross-platform", + "id": "owBYojOVzZU-pjscj82gQAHvhUDTMgzQtTcQjyBpzHT-bqLwtLF2OOJDoskE18lOn2-1-SV-b7nCvn5s5Uq2KhBt1Q9kFVBUsb8jBl959BY3KWTg2rgjpN9nB5uIWTEFXfAWo0qIYGGVhXLyEbvu72Lq_W0wlccoKlxWrP349qN9OG2RTaGrgNjxTo1LqnSVc9S6D1zD7mop5KQ_9FZEjA5jABAquwFMAuO4ongyujnpoAfyAlB6UZ_JDmDFCkuN598q_LAu", + "rawId": "owBYojOVzZU-pjscj82gQAHvhUDTMgzQtTcQjyBpzHT-bqLwtLF2OOJDoskE18lOn2-1-SV-b7nCvn5s5Uq2KhBt1Q9kFVBUsb8jBl959BY3KWTg2rgjpN9nB5uIWTEFXfAWo0qIYGGVhXLyEbvu72Lq_W0wlccoKlxWrP349qN9OG2RTaGrgNjxTo1LqnSVc9S6D1zD7mop5KQ_9FZEjA5jABAquwFMAuO4ongyujnpoAfyAlB6UZ_JDmDFCkuN598q_LAu", "response": { - "authenticatorData":"o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcFAAAAXA", - "clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWjE2T2hrVlB5d245Mjc2SjZ3dEdmZyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJjcm9zc09yaWdpbiI6dHJ1ZX0", - "signature":"9frQigpe0p8NGwWc9Ikve9RlOZbcmz6S-JVDaPde-dxS-sPRFLGDA3ekh0j294MqaejRudzTw5uggh1IU2lJCQ", - "userHandle": None - } + "authenticatorData": "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcFAAAAXA", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWjE2T2hrVlB5d245Mjc2SjZ3dEdmZyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJjcm9zc09yaWdpbiI6dHJ1ZX0", + "signature": "9frQigpe0p8NGwWc9Ikve9RlOZbcmz6S-JVDaPde-dxS-sPRFLGDA3ekh0j294MqaejRudzTw5uggh1IU2lJCQ", + "userHandle": None, + }, } user = { @@ -254,25 +265,29 @@ def test_get_credential(self): "uv_initialized": True, } options = { - 'challenge': "Z16OhkVPywn9276J6wtGfg", - 'rpId': 'example.com', + "challenge": "Z16OhkVPywn9276J6wtGfg", + "rpId": "example.com", "allowCredentials": [ { "type": "public-key", - "id": ("owBYojOVzZU-pjscj82gQAHvhUDTMgzQtTcQjyBpzHT-bqLwtLF2OOJDoskE18lO" - "n2-1-SV-b7nCvn5s5Uq2KhBt1Q9kFVBUsb8jBl959BY3KWTg2rgjpN9nB5uIWTEF" - "XfAWo0qIYGGVhXLyEbvu72Lq_W0wlccoKlxWrP349qN9OG2RTaGrgNjxTo1LqnSV" - "c9S6D1zD7mop5KQ_9FZEjA5jABAquwFMAuO4ongyujnpoAfyAlB6UZ_JDmDFCkuN" - "598q_LAu") + "id": ( + "owBYojOVzZU-pjscj82gQAHvhUDTMgzQtTcQjyBpzHT-bqLwtLF2OOJDoskE18lO" + "n2-1-SV-b7nCvn5s5Uq2KhBt1Q9kFVBUsb8jBl959BY3KWTg2rgjpN9nB5uIWTEF" + "XfAWo0qIYGGVhXLyEbvu72Lq_W0wlccoKlxWrP349qN9OG2RTaGrgNjxTo1LqnSV" + "c9S6D1zD7mop5KQ_9FZEjA5jABAquwFMAuO4ongyujnpoAfyAlB6UZ_JDmDFCkuN" + "598q_LAu" + ), }, ], } - expected_origin = 'https://example.com' + expected_origin = "https://example.com" - auth_data = webauthn.verify_get_response(response, options, "https://example.com", user, None) - self.assertTrue(auth_data.has_flag('UV')) - self.assertFalse(auth_data.has_flag('BS')) - self.assertTrue(auth_data.sign_count > user['sign_count']) + auth_data = webauthn.verify_get_response( + response, options, "https://example.com", user, None + ) + self.assertTrue(auth_data.has_flag("UV")) + self.assertFalse(auth_data.has_flag("BS")) + self.assertTrue(auth_data.sign_count > user["sign_count"]) def test_create_u2f_credential(self): response = { @@ -286,42 +301,33 @@ def test_create_u2f_credential(self): "transports": ["usb"], "authenticatorData": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v-ppaZJdA7dBAAAAAAAAAAAAAAAAAAAAAAAAAAAAYPGdjuzMVoVnLKvs08iEfTsqDQNaH5yP84sva5WKMzF3AErD10K6lVKV49Z6ZM1jiJOsjoII4n06ppkfWQONJvujW0vN_XAsBqE9nFZpOKdzlj5SYoX95lJ0psxRe6RtoqUBAgMmIAEhWCBCRSlLPOaY_h1eAuKoX-k_bX9Vxmnrt_08jiGRbws9hiJYIJG3xgod6abzVyI9E6QioArtDscVBH56lABGF8ojHxov", "publicKey": "pQECAyYgASFYIEJFKUs85pj-HV4C4qhf6T9tf1XGaeu3_TyOIZFvCz2GIlggkbfGCh3ppvNXIj0TpCKgCu0OxxUEfnqUAEYXyiMfGi8", - "publicKeyAlgorithm": -7 - } + "publicKeyAlgorithm": -7, + }, } - challenge = 'LSwXTIRzLFsL3iSrg7gj0rolzVxlUJ1GwCqD0tu6rxk' + challenge = "LSwXTIRzLFsL3iSrg7gj0rolzVxlUJ1GwCqD0tu6rxk" create_options = { "attestation": "direct", "authenticatorSelection": { "authenticatorAttachment": "cross-platform", "requireResidentKey": False, "residentKey": "discouraged", - "userVerification": "discouraged" + "userVerification": "discouraged", }, "challenge": challenge, "excludeCredentials": [], "pubKeyCredParams": [ - { - "alg": -7, - "type": "public-key" - }, - { - "alg": -257, - "type": "public-key" - } + {"alg": -7, "type": "public-key"}, + {"alg": -257, "type": "public-key"}, ], - "rp": { - "id": "demo.yubico.com", - "name": "Yubico Demo" - }, + "rp": {"id": "demo.yubico.com", "name": "Yubico Demo"}, "timeout": 600000, "user": { "displayName": "qwelvy", "id": "NfF0j0oEdzdAkGD1kxrQCzw-X6ryVIpcAISt8RoToxU", - "name": "qwelvy" - } + "name": "qwelvy", + }, } origin = "https://demo.yubico.com" auth_data = webauthn.verify_create_response(response, create_options, origin) - self.assertEqual(response['id'], util.b64_encode(auth_data.cred_id)) + self.assertEqual(response["id"], util.b64_encode(auth_data.cred_id)) From 92d874625ff8ce0c777846038aee2a700c859015 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 2 Aug 2025 08:18:34 -0500 Subject: [PATCH 23/38] Work on combining init_request/complete_auth into a single method call for public service --- credsd/src/credential_service/mod.rs | 59 +++++--- credsd/src/credential_service/server.rs | 12 +- credsd/src/dbus.rs | 171 +++++++++++++++++++----- 3 files changed, 186 insertions(+), 56 deletions(-) diff --git a/credsd/src/credential_service/mod.rs b/credsd/src/credential_service/mod.rs index 03bb7a3b..832827e2 100644 --- a/credsd/src/credential_service/mod.rs +++ b/credsd/src/credential_service/mod.rs @@ -14,11 +14,17 @@ use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, }; -use tokio::sync::Mutex as AsyncMutex; +use tokio::sync::{ + mpsc::{self, Receiver, Sender}, + Mutex as AsyncMutex, +}; use creds_lib::{ client::CredentialServiceClient, - model::{CredentialRequest, CredentialResponse, Device, Operation, Transport}, + model::{ + CredentialRequest, CredentialResponse, Device, Error as CredentialServiceError, Operation, + Transport, + }, server::{CreatePublicKeyCredentialRequest, ViewRequest}, }; @@ -35,7 +41,12 @@ pub use { pub struct CredentialService { devices: Vec, - cred_request: Mutex>, + cred_request: Mutex< + Option<( + CredentialRequest, + Sender>, + )>, + >, // Place to store data to be returned to the caller cred_response: Arc>>, @@ -72,14 +83,28 @@ impl } } - pub async fn init_request(&self, request: &CredentialRequest) -> Result<(), String> { - { + pub async fn init_request( + &self, + request: &CredentialRequest, + ) -> Receiver> { + let (tx, rx) = mpsc::channel(1); + let res = { let mut cred_request = self.cred_request.lock().unwrap(); if cred_request.is_some() { - return Err("Already a request in progress.".to_string()); + drop(cred_request); + false + } else { + _ = cred_request.insert((request.clone(), tx.clone())); + true } - - _ = cred_request.insert(request.clone()); + }; + if !res { + tx.send(Err(CredentialServiceError::Internal( + "Already a request in progress.".to_string(), + ))) + .await + .expect("Send to local receiver to succeed"); + return rx; } let operation = match &request { CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, @@ -92,15 +117,13 @@ impl .launch_ui(view_request) .await .map_err(|err| err.to_string()); - match launch_ui_response { - Ok(()) => Ok(()), - Err(err) => { - tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); - let mut cred_request = self.cred_request.lock().unwrap(); - _ = cred_request.take(); - Err(err) - } + if let Err(err) = launch_ui_response { + tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); + _ = self.cred_request.lock().unwrap().take(); + let err = Err(CredentialServiceError::Internal(err)); + tx.send(err).await; } + return rx; } pub async fn get_available_public_key_devices(&self) -> Result, ()> { @@ -112,7 +135,7 @@ impl ) -> Pin + Send + 'static>> { let guard = self.cred_request.lock().unwrap(); let cred_request = guard.clone().unwrap(); - let stream = self.hybrid_handler.start(&cred_request); + let stream = self.hybrid_handler.start(&cred_request.0); let cred_response = self.cred_response.clone(); Box::pin(HybridStateStream { inner: stream, @@ -123,7 +146,7 @@ impl pub fn get_usb_credential(&self) -> Pin + Send + 'static>> { let guard = self.cred_request.lock().unwrap(); let cred_request = guard.clone().unwrap(); - let stream = self.usb_handler.start(&cred_request); + let stream = self.usb_handler.start(&cred_request.0); Box::pin(UsbStateStream { inner: stream, cred_response: self.cred_response.clone(), diff --git a/credsd/src/credential_service/server.rs b/credsd/src/credential_service/server.rs index 7ab9e2c5..d3c7343e 100644 --- a/credsd/src/credential_service/server.rs +++ b/credsd/src/credential_service/server.rs @@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex}; use creds_lib::client::CredentialServiceClient; use creds_lib::server::ViewRequest; use futures_lite::{Stream, StreamExt}; +use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; use creds_lib::model::{BackgroundEvent, CredentialRequest, CredentialResponse, Device}; @@ -25,7 +26,7 @@ enum ManagementRequest { enum ManagementResponse { EnterClientPin, - InitRequest(Result<(), String>), + InitRequest(Receiver>), CompleteAuth(Result), GetDevices(Vec), GetHybridCredential, @@ -101,7 +102,7 @@ pub trait CredentialManagementClient { fn init_request( &self, cred_request: CredentialRequest, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; fn complete_auth(&self) -> impl Future> + Send; fn get_available_public_key_devices( @@ -190,7 +191,10 @@ impl< UC: UiController + Debug + Send + Sync, > CredentialManagementClient for InProcessManager { - async fn init_request(&self, cred_request: CredentialRequest) -> Result<(), String> { + async fn init_request( + &self, + cred_request: CredentialRequest, + ) -> Receiver> { self.svc.lock().await.init_request(&cred_request).await } @@ -458,6 +462,7 @@ where oneshot::Sender, )>, mgr: InProcessManager, + responder_rx: Option>>, } impl InProcessServer @@ -486,6 +491,7 @@ where let server = Self { rx, mgr: mgr.clone(), + responder_rx: None, }; (server, mgr, client) } diff --git a/credsd/src/dbus.rs b/credsd/src/dbus.rs index 2c729707..2dccdb86 100644 --- a/credsd/src/dbus.rs +++ b/credsd/src/dbus.rs @@ -34,15 +34,16 @@ mod model; use std::pin::Pin; use std::{collections::VecDeque, error::Error, fmt::Debug, sync::Arc}; -use creds_lib::server::{CreateCredentialRequest, ViewRequest}; +use creds_lib::model::MakeCredentialRequest; +use creds_lib::server::{CreateCredentialRequest, CreatePublicKeyCredentialRequest, ViewRequest}; use futures_lite::{Stream, StreamExt}; -use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::Mutex as AsyncMutex; use tokio::task::AbortHandle; use zbus::object_server::{InterfaceRef, SignalEmitter}; use zbus::{ connection::{self, Connection}, - fdo, interface, Result, + fdo, interface, }; use zbus::{proxy, ObjectServer}; @@ -72,7 +73,7 @@ pub(crate) async fn start_service Result { +) -> zbus::Result { let lock = Arc::new(AsyncMutex::new(())); connection::Builder::session()? .name(service_name)? @@ -244,7 +245,7 @@ pub async fn start_internal_service< service_name: &str, path: &str, credential_service: CredentialService, -) -> Result { +) -> zbus::Result { connection::Builder::session()? .name(service_name)? .serve_at( @@ -261,6 +262,100 @@ pub async fn start_internal_service< .await } +struct CredentialRequestController { + svc: Arc>>, +} + +#[interface(name = "xyz.iinuwa.credentials.impl.Credentials")] +impl CredentialRequestController +where + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +{ + async fn create_credential( + &self, + request: CreateCredentialRequest, + ) -> fdo::Result { + match create_credential_request_try_into_ctap2(&request) { + Ok((make_request, client_data_json)) => { + let mut rx = { + let rx: Receiver> = self + .svc + .lock() + .await + .init_request(&CredentialRequest::CreatePublicKeyCredentialRequest( + make_request, + )) + .await; + rx + }; + let msg = rx.recv().await.ok_or_else(|| { + tracing::error!("Credential service shutdown response channel prematurely"); + fdo::Error::Failed("Credential service shutdown".to_string()) + })?; + match msg { + Ok(CredentialResponse::CreatePublicKeyCredentialResponse(cred_response)) => { + let public_key_response = create_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; + Ok(public_key_response.into()) + } + // We should be returning the correct kind of response, so this shouldn't happen. + Ok(_) => Err(fdo::Error::Failed("Internal error occurred".to_string())), + Err(_) => Err(fdo::Error::Failed( + "Failed to create credential".to_string(), + )), + } + } + Err(_) => Err(fdo::Error::InvalidArgs( + "Unable to parse create credential request".to_string(), + )), + } + } + + async fn get_credential( + &self, + request: GetCredentialRequest, + ) -> fdo::Result { + match get_credential_request_try_into_ctap2(&request) { + Ok((get_request, client_data_json)) => { + let mut rx = { + let rx: Receiver> = self + .svc + .lock() + .await + .init_request(&CredentialRequest::GetPublicKeyCredentialRequest( + get_request, + )) + .await; + rx + }; + let msg = rx.recv().await.ok_or_else(|| { + tracing::error!("Credential service shutdown response channel prematurely"); + fdo::Error::Failed("Credential service shutdown".to_string()) + })?; + match msg { + Ok(CredentialResponse::GetPublicKeyCredentialResponse(cred_response)) => { + let public_key_response = get_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; + Ok(public_key_response.into()) + } + // We should be returning the correct kind of response, so this shouldn't happen. + Ok(_) => Err(fdo::Error::Failed("Internal error occurred".to_string())), + Err(_) => Err(fdo::Error::Failed("Failed to get credential".to_string())), + } + } + Err(_) => Err(fdo::Error::InvalidArgs( + "Unable to parse get credential request".to_string(), + )), + } + } +} + pub struct InternalService { signal_state: Arc>, svc: Arc>>, @@ -326,7 +421,7 @@ where let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: Result>> = object_server + let interface: zbus::Result>> = object_server .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") .await; @@ -378,7 +473,7 @@ where let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: Result>> = object_server + let interface: zbus::Result>> = object_server .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") .await; @@ -470,30 +565,32 @@ pub struct CredentialControlServiceClient { } impl CredentialControlServiceClient { - async fn proxy(&self) -> Result { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + + async fn proxy(&self) -> zbus::Result { InternalServiceProxy::new(&self.conn).await } } impl CredentialManagementClient for CredentialControlServiceClient { - fn init_request( + async fn init_request( &self, cred_request: CredentialRequest, - ) -> impl std::prelude::rust_2024::Future> + Send { - todo!() + ) -> Receiver> { + // TODO: Start here + self.proxy().await.unwrap(). } - fn complete_auth( - &self, - ) -> impl std::prelude::rust_2024::Future> - + Send { + async fn complete_auth(&self) -> Result { todo!() } async fn get_available_public_key_devices( &self, - ) -> std::result::Result, Box> { - let devices: std::result::Result, String> = self + ) -> Result, Box> { + let devices: Result, String> = self .proxy() .await? .get_available_public_key_devices() @@ -504,31 +601,30 @@ impl CredentialManagementClient for CredentialControlServiceClient { Ok(devices?) } - async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { + async fn get_hybrid_credential(&mut self) -> Result<(), ()> { todo!() } - async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { + async fn get_usb_credential(&mut self) -> Result<(), ()> { todo!() } async fn initiate_event_stream( &mut self, - ) -> std::result::Result< - Pin + Send + 'static>>, - (), - > { + ) -> Result + Send + 'static>>, ()> + { todo!() } - async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { - if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { - pin_tx.send(pin).await.unwrap(); + async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { + if let Err(err) = self.proxy().await.unwrap().enter_client_pin(pin).await { + tracing::error!("Failed to send client pin: {err}"); + return Err(()); } Ok(()) } - async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { + async fn select_credential(&self, credential_id: String) -> Result<(), ()> { todo!() } } @@ -554,12 +650,12 @@ impl UiControlServiceClient { Self { conn } } - async fn proxy(&self) -> std::result::Result { + async fn proxy(&self) -> Result { UiControlServiceClientProxy::new(&self.conn).await } } impl UiController for UiControlServiceClient { - async fn launch_ui(&self, request: ViewRequest) -> std::result::Result<(), Box> { + async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { self.proxy() .await? .launch_ui(request) @@ -573,26 +669,30 @@ async fn execute_flow( // gui_tx: &async_std::channel::Sender, manager_client: &C, cred_request: &CredentialRequest, -) -> Result { - manager_client - .init_request(cred_request.clone()) +) -> zbus::Result { + let mut signal_rx = manager_client.init_request(cred_request.clone()).await; + let rsp = signal_rx + .recv() .await - .map_err(|_| fdo::Error::Failed("Request already running".to_string()))?; + .ok_or(fdo::Error::Failed( + "Credential service unexpectedly interrupted".to_string(), + ))? + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + Ok(rsp) + /* // start GUI let operation = match &cred_request { CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, }; let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); - /* let view_request = ViewRequest { operation, signal: signal_tx, }; // TODO: Replace this with a UiControlClient // 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()) @@ -603,4 +703,5 @@ async fn execute_flow( tracing::error!("Error retrieving credential: {:?}", err); zbus::Error::Failure("Error retrieving credential".to_string()) }) + */ } From 15e52ca0731cfc71d11067808cbf62dc943b4b6b Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 2 Aug 2025 08:26:29 -0500 Subject: [PATCH 24/38] Move dbus.rs into dbus/mod.rs --- credsd/src/{dbus.rs => dbus/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename credsd/src/{dbus.rs => dbus/mod.rs} (100%) diff --git a/credsd/src/dbus.rs b/credsd/src/dbus/mod.rs similarity index 100% rename from credsd/src/dbus.rs rename to credsd/src/dbus/mod.rs From 4c791a5ed1978efa5e3b0c9751355158ad38800c Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 07:37:59 -0500 Subject: [PATCH 25/38] wip: Move D-Bus services into separate modules --- creds-lib/src/model.rs | 37 ++ credsd/src/dbus/broker.rs | 179 +++++++++ credsd/src/dbus/flow_control.rs | 272 ++++++++++++++ credsd/src/dbus/gateway.rs | 144 ++++++++ credsd/src/dbus/mod.rs | 631 +------------------------------- credsd/src/dbus/ui_control.rs | 34 ++ 6 files changed, 670 insertions(+), 627 deletions(-) create mode 100644 credsd/src/dbus/broker.rs create mode 100644 credsd/src/dbus/flow_control.rs create mode 100644 credsd/src/dbus/gateway.rs create mode 100644 credsd/src/dbus/ui_control.rs diff --git a/creds-lib/src/model.rs b/creds-lib/src/model.rs index c7a564c4..97b5fde3 100644 --- a/creds-lib/src/model.rs +++ b/creds-lib/src/model.rs @@ -294,3 +294,40 @@ impl Display for Error { } } } + +pub enum WebAuthnError { + /// The ceremony was cancelled by an AbortController. See § 5.6 Abort + /// Operations with AbortSignal and § 1.3.4 Aborting Authentication + /// Operations. + AbortError, + + /// Either `residentKey` was set to required and no available authenticator + /// supported resident keys, or `userVerification` was set to required and no + /// available authenticator could perform user verification. + ConstraintError, + + /// The authenticator used in the ceremony recognized an entry in + /// `excludeCredentials` after the user consented to registering a credential. + InvalidStateError, + + /// No entry in `pubKeyCredParams` had a type property of `public-key`, or the + /// authenticator did not support any of the signature algorithms specified + /// in `pubKeyCredParams`. + NotSupportedError, + + /// The effective domain was not a valid domain, or `rp.id` was not equal to + /// or a registrable domain suffix of the effective domain. In the latter + /// case, the client does not support related origin requests or the related + /// origins validation procedure failed. + SecurityError, + + /// A catch-all error covering a wide range of possible reasons, including + /// common ones like the user canceling out of the ceremony. Some of these + /// causes are documented throughout this spec, while others are + /// client-specific. + NotAllowedError, + + /// The options argument was not a valid `CredentialCreationOptions` value, or + /// the value of `user.id` was empty or was longer than 64 bytes. + TypeError, +} diff --git a/credsd/src/dbus/broker.rs b/credsd/src/dbus/broker.rs new file mode 100644 index 00000000..eb01576d --- /dev/null +++ b/credsd/src/dbus/broker.rs @@ -0,0 +1,179 @@ +pub(crate) async fn start_service( + service_name: &str, + path: &str, + manager_client: C, +) -> zbus::Result { + let lock = Arc::new(AsyncMutex::new(())); + connection::Builder::session()? + .name(service_name)? + .serve_at( + path, + CredentialManager { + app_lock: lock, + manager_client, + }, + )? + .build() + .await +} + +pub async fn start_internal_service< + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +>( + service_name: &str, + path: &str, + credential_service: CredentialService, +) -> zbus::Result { + connection::Builder::session()? + .name(service_name)? + .serve_at( + path, + InternalService { + signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), + svc: Arc::new(AsyncMutex::new(credential_service)), + usb_pin_tx: Arc::new(AsyncMutex::new(None)), + usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), + hybrid_event_forwarder_task: Arc::new(AsyncMutex::new(None)), + }, + )? + .build() + .await +} + +struct CredentialRequestController { + svc: Arc>>, +} + +#[interface(name = "xyz.iinuwa.credentials.impl.Credentials")] +impl CredentialRequestController +where + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +{ + async fn create_credential( + &self, + request: CreateCredentialRequest, + ) -> fdo::Result { + match create_credential_request_try_into_ctap2(&request) { + Ok((make_request, client_data_json)) => { + let mut rx = { + let rx: Receiver> = self + .svc + .lock() + .await + .init_request(&CredentialRequest::CreatePublicKeyCredentialRequest( + make_request, + )) + .await; + rx + }; + let msg = rx.recv().await.ok_or_else(|| { + tracing::error!("Credential service shutdown response channel prematurely"); + fdo::Error::Failed("Credential service shutdown".to_string()) + })?; + match msg { + Ok(CredentialResponse::CreatePublicKeyCredentialResponse(cred_response)) => { + let public_key_response = create_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; + Ok(public_key_response.into()) + } + // We should be returning the correct kind of response, so this shouldn't happen. + Ok(_) => Err(fdo::Error::Failed("Internal error occurred".to_string())), + Err(_) => Err(fdo::Error::Failed( + "Failed to create credential".to_string(), + )), + } + } + Err(_) => Err(fdo::Error::InvalidArgs( + "Unable to parse create credential request".to_string(), + )), + } + } + + async fn get_credential( + &self, + request: GetCredentialRequest, + ) -> fdo::Result { + match get_credential_request_try_into_ctap2(&request) { + Ok((get_request, client_data_json)) => { + let mut rx = { + let rx: Receiver> = self + .svc + .lock() + .await + .init_request(&CredentialRequest::GetPublicKeyCredentialRequest( + get_request, + )) + .await; + rx + }; + let msg = rx.recv().await.ok_or_else(|| { + tracing::error!("Credential service shutdown response channel prematurely"); + fdo::Error::Failed("Credential service shutdown".to_string()) + })?; + match msg { + Ok(CredentialResponse::GetPublicKeyCredentialResponse(cred_response)) => { + let public_key_response = get_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; + Ok(public_key_response.into()) + } + // We should be returning the correct kind of response, so this shouldn't happen. + Ok(_) => Err(fdo::Error::Failed("Internal error occurred".to_string())), + Err(_) => Err(fdo::Error::Failed("Failed to get credential".to_string())), + } + } + Err(_) => Err(fdo::Error::InvalidArgs( + "Unable to parse get credential request".to_string(), + )), + } + } +} + +async fn execute_flow( + // TODO: Replace this with UiControlClient + // gui_tx: &async_std::channel::Sender, + manager_client: &C, + cred_request: &CredentialRequest, +) -> zbus::Result { + let mut signal_rx = manager_client.init_request(cred_request.clone()).await; + let rsp = signal_rx + .recv() + .await + .ok_or(fdo::Error::Failed( + "Credential service unexpectedly interrupted".to_string(), + ))? + .map_err(|err| fdo::Error::Failed(err.to_string()))?; + Ok(rsp) + + /* + // start GUI + let operation = match &cred_request { + CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, + CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, + }; + let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); + let view_request = ViewRequest { + operation, + signal: signal_tx, + }; + // TODO: Replace this with a UiControlClient + // 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()) + }) + */ +} diff --git a/credsd/src/dbus/flow_control.rs b/credsd/src/dbus/flow_control.rs new file mode 100644 index 00000000..afcc524f --- /dev/null +++ b/credsd/src/dbus/flow_control.rs @@ -0,0 +1,272 @@ +pub struct InternalService { + signal_state: Arc>, + svc: Arc>>, + usb_pin_tx: Arc>>>, + usb_event_forwarder_task: Arc>>, + hybrid_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. +#[interface( + name = "xyz.iinuwa.credentials.CredentialManagerInternal1", + proxy( + gen_blocking = false, + default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", + default_service = "xyz.iinuwa.credentials.CredentialManagerInternal", + ) +)] +impl InternalService +where + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +{ + async fn initiate_event_stream( + &self, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> fdo::Result<()> { + let mut signal_state = self.signal_state.lock().await; + match *signal_state { + SignalState::Idle => {} + SignalState::Pending(ref mut pending) => { + for msg in pending.iter_mut() { + emitter.state_changed(msg.clone()).await?; + } + } + SignalState::Active => {} + }; + *signal_state = SignalState::Active; + Ok(()) + } + + async fn get_available_public_key_devices(&self) -> fdo::Result> { + let devices = self + .svc + .lock() + .await + .get_available_public_key_devices() + .await + .map_err(|_| { + fdo::Error::Failed("Failed to get retrieve available devices".to_string()) + })?; + Ok(devices.into_iter().map(Device::from).collect()) + } + + async fn get_hybrid_credential( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + ) -> fdo::Result<()> { + let svc = self.svc.lock().await; + let mut stream = svc.get_hybrid_credential(); + let signal_state = self.signal_state.clone(); + let object_server = object_server.clone(); + let task = tokio::spawn(async move { + let interface: zbus::Result>> = object_server + .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") + .await; + + let emitter = match interface { + Ok(ref i) => i.signal_emitter(), + Err(err) => { + tracing::error!("Failed to get connection to D-Bus to send signals: {err}"); + return; + } + }; + while let Some(state) = stream.next().await { + let event = + creds_lib::model::BackgroundEvent::HybridQrStateChanged(state.clone().into()) + .try_into(); + match event { + Err(err) => { + tracing::error!("Failed to serialize state update: {err}"); + break; + } + Ok(event) => match send_state_update(&emitter, &signal_state, event).await { + Ok(_) => {} + Err(err) => { + tracing::error!("Failed to send state update to UI: {err}"); + break; + } + }, + } + match state { + HybridState::Completed | HybridState::Failed => { + break; + } + _ => {} + }; + } + }) + .abort_handle(); + if let Some(prev_task) = self.hybrid_event_forwarder_task.lock().await.replace(task) { + prev_task.abort(); + } + Ok(()) + } + + async fn get_usb_credential( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + ) -> fdo::Result<()> { + let mut stream = self.svc.lock().await.get_usb_credential(); + let usb_pin_tx = self.usb_pin_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>> = object_server + .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") + .await; + + let emitter = match interface { + Ok(ref i) => i.signal_emitter(), + Err(err) => { + tracing::error!("Failed to get connection to D-Bus to send signals: {err}"); + return; + } + }; + while let Some(state) = stream.next().await { + match creds_lib::model::BackgroundEvent::UsbStateChanged((&state).into()).try_into() + { + Err(err) => { + tracing::error!("Failed to serialize state update: {err}"); + break; + } + Ok(event) => match send_state_update(&emitter, &signal_state, event).await { + Ok(_) => {} + Err(err) => { + tracing::error!("Failed to send state update to UI: {err}"); + break; + } + }, + }; + match state { + UsbState::NeedsPin { pin_tx, .. } => { + let mut usb_pin_tx = usb_pin_tx.lock().await; + let _ = usb_pin_tx.insert(pin_tx); + } + UsbState::Completed | UsbState::Failed(_) => { + break; + } + _ => {} + }; + } + }) + .abort_handle(); + if let Some(prev_task) = self.usb_event_forwarder_task.lock().await.replace(task) { + prev_task.abort(); + } + Ok(()) + } + + async fn select_device(&self, device_id: String) -> fdo::Result<()> { + todo!() + } + + async fn enter_client_pin(&self, pin: String) -> fdo::Result<()> { + if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { + pin_tx.send(pin).await.unwrap(); + } + Ok(()) + } + + async fn select_credential(&self, credential_id: String) -> fdo::Result<()> { + todo!() + } + + #[zbus(signal)] + async fn state_changed( + emitter: &SignalEmitter<'_>, + update: BackgroundEvent, + ) -> zbus::Result<()>; +} +async fn send_state_update( + emitter: &SignalEmitter<'_>, + signal_state: &Arc>, + update: BackgroundEvent, +) -> fdo::Result<()> { + let mut signal_state = signal_state.lock().await; + match *signal_state { + SignalState::Idle => { + let pending = VecDeque::from([update]); + *signal_state = SignalState::Pending(pending); + } + SignalState::Pending(ref mut pending) => { + pending.push_back(update); + } + SignalState::Active => { + emitter.state_changed(update).await?; + } + }; + Ok(()) +} + +pub struct CredentialControlServiceClient { + conn: Connection, +} + +impl CredentialControlServiceClient { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + + async fn proxy(&self) -> zbus::Result { + InternalServiceProxy::new(&self.conn).await + } +} + +impl CredentialManagementClient for CredentialControlServiceClient { + async fn init_request( + &self, + cred_request: CredentialRequest, + ) -> Receiver> { + // TODO: Start here + self.proxy().await.unwrap(). + } + + async fn complete_auth(&self) -> Result { + todo!() + } + + async fn get_available_public_key_devices( + &self, + ) -> Result, Box> { + let devices: Result, String> = self + .proxy() + .await? + .get_available_public_key_devices() + .await? + .into_iter() + .map(|d| d.try_into().map_err(|_| "Failed".to_string())) + .collect(); + Ok(devices?) + } + + async fn get_hybrid_credential(&mut self) -> Result<(), ()> { + todo!() + } + + async fn get_usb_credential(&mut self) -> Result<(), ()> { + todo!() + } + + async fn initiate_event_stream( + &mut self, + ) -> Result + Send + 'static>>, ()> + { + todo!() + } + + async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { + if let Err(err) = self.proxy().await.unwrap().enter_client_pin(pin).await { + tracing::error!("Failed to send client pin: {err}"); + return Err(()); + } + Ok(()) + } + + async fn select_credential(&self, credential_id: String) -> Result<(), ()> { + todo!() + } +} diff --git a/credsd/src/dbus/gateway.rs b/credsd/src/dbus/gateway.rs new file mode 100644 index 00000000..28f619ea --- /dev/null +++ b/credsd/src/dbus/gateway.rs @@ -0,0 +1,144 @@ +//! Implements the service that public clients can connect to. Responsible for +//! authorizing clients for origins and validating request parameters. + +use std::sync::Arc; + +use creds_lib::{ + model::{CredentialRequest, CredentialResponse, GetClientCapabilitiesResponse, WebAuthnError}, + server::{ + CreateCredentialRequest, CreateCredentialResponse, GetCredentialRequest, + GetCredentialResponse, + }, +}; +use tokio::sync::Mutex as AsyncMutex; +use zbus::{fdo, interface}; + +use crate::dbus::{ + create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, + get_credential_request_try_into_ctap2, +}; + +struct CredentialGateway { + controller: Arc>, +} + +/// These are public methods that can be called by arbitrary clients to begin a credential flow. +#[interface(name = "xyz.iinuwa.credentials.Credentials1")] +impl CredentialGateway { + async fn create_credential( + &self, + request: CreateCredentialRequest, + ) -> fdo::Result { + if request.origin.is_none() { + todo!("Implicit caller-origin binding not yet implemented.") + }; + let is_same_origin = request.is_same_origin.unwrap_or(false); + let response = match (request.r#type.as_ref(), &request.public_key) { + ("publicKey", Some(_)) => { + if !is_same_origin { + return Err(fdo::Error::AccessDenied(String::from( + "Cross-origin public-key credentials are not allowed.", + ))); + } + let (make_cred_request, client_data_json) = + create_credential_request_try_into_ctap2(&request).map_err(|e| { + fdo::Error::Failed(format!( + "Could not parse passkey creation request: {e:?}" + )) + })?; + let cred_request = + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); + + let response = execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; + + if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = + response + { + let public_key_response = create_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; + Ok(public_key_response.into()) + } else { + Err(fdo::Error::Failed("Failed to create passkey".to_string())) + } + } + _ => Err(fdo::Error::Failed( + "Unknown credential request type".to_string(), + )), + }; + response + } + + async fn get_credential( + &self, + request: GetCredentialRequest, + ) -> fdo::Result { + if request.origin.is_none() { + todo!("Implicit caller-origin binding is not yet implemented."); + } + let is_same_origin = request.is_same_origin.unwrap_or(false); + let response = match (request.r#type.as_ref(), &request.public_key) { + ("publicKey", Some(_)) => { + if !is_same_origin { + return Err(fdo::Error::AccessDenied(String::from( + "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 + // - if RP ID is set, but origin's effective domain doesn't match + // - query for related origins, if supported + // - fail if not supported, or if RP ID doesn't match any related origins. + let (get_cred_request, client_data_json) = + get_credential_request_try_into_ctap2(&request).map_err(|_| { + fdo::Error::Failed("Could not parse passkey assertion request.".to_owned()) + })?; + let cred_request = + CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); + + let response = execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; + + match response { + CredentialResponse::GetPublicKeyCredentialResponse(cred_response) => { + let public_key_response = get_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + )?; + Ok(public_key_response.into()) + } + _ => Err(fdo::Error::Failed( + "Invalid credential response received from authenticator".to_string(), + )), + } + } + _ => Err(fdo::Error::Failed( + "Unknown credential request type".to_string(), + )), + }; + response + } + + async fn get_client_capabilities(&self) -> fdo::Result { + Ok(GetClientCapabilitiesResponse { + conditional_create: false, + conditional_get: false, + hybrid_transport: true, + passkey_platform_authenticator: false, + user_verifying_platform_authenticator: false, + related_origins: false, + signal_all_accepted_credentials: false, + signal_current_user_details: false, + signal_unknown_credential: false, + }) + } +} + +trait CredentialRequestController { + async fn request_credential( + request: CredentialRequest, + ) -> Result; +} diff --git a/credsd/src/dbus/mod.rs b/credsd/src/dbus/mod.rs index 2dccdb86..4d51a37b 100644 --- a/credsd/src/dbus/mod.rs +++ b/credsd/src/dbus/mod.rs @@ -29,7 +29,11 @@ //! - launch UI //! - send_state_changed() +mod broker; +mod flow_control; +mod gateway; mod model; +mod ui_control; use std::pin::Pin; use std::{collections::VecDeque, error::Error, fmt::Debug, sync::Arc}; @@ -69,25 +73,6 @@ use crate::credential_service::{ CredentialManagementClient, CredentialService, UiController, UsbState, }; -pub(crate) async fn start_service( - service_name: &str, - path: &str, - manager_client: C, -) -> zbus::Result { - let lock = Arc::new(AsyncMutex::new(())); - connection::Builder::session()? - .name(service_name)? - .serve_at( - path, - CredentialManager { - app_lock: lock, - manager_client, - }, - )? - .build() - .await -} - enum SignalState { /// No state Idle, @@ -97,611 +82,3 @@ enum SignalState { /// Client is actively receiving messages. Active, } - -struct CredentialManager { - // app_lock: Arc>>, - app_lock: Arc>, - manager_client: C, -} - -/// These are public methods that can be called by arbitrary clients to begin a credential flow. -#[interface(name = "xyz.iinuwa.credentials.Credentials1")] -impl CredentialManager { - async fn create_credential( - &self, - request: CreateCredentialRequest, - ) -> fdo::Result { - if let Ok(tx) = self.app_lock.try_lock() { - if request.origin.is_none() { - todo!("Implicit caller-origin binding not yet implemented.") - }; - let is_same_origin = request.is_same_origin.unwrap_or(false); - let response = match (request.r#type.as_ref(), &request.public_key) { - ("publicKey", Some(_)) => { - if !is_same_origin { - return Err(fdo::Error::AccessDenied(String::from( - "Cross-origin public-key credentials are not allowed.", - ))); - } - let (make_cred_request, client_data_json) = - create_credential_request_try_into_ctap2(&request).map_err(|e| { - fdo::Error::Failed(format!( - "Could not parse passkey creation request: {e:?}" - )) - })?; - let cred_request = - CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); - - let response = - execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; - - if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = - response - { - let public_key_response = create_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - )?; - Ok(public_key_response.into()) - } else { - Err(fdo::Error::Failed("Failed to create passkey".to_string())) - } - } - _ => Err(fdo::Error::Failed( - "Unknown credential request type".to_string(), - )), - }; - response - } else { - tracing::info!("Window already open"); - Err(fdo::Error::ObjectPathInUse( - "WebAuthn session already open.".into(), - )) - } - } - - async fn get_credential( - &self, - request: GetCredentialRequest, - ) -> fdo::Result { - if let Ok(tx) = self.app_lock.try_lock() { - if request.origin.is_none() { - todo!("Implicit caller-origin binding is not yet implemented."); - } - let is_same_origin = request.is_same_origin.unwrap_or(false); - let response = match (request.r#type.as_ref(), &request.public_key) { - ("publicKey", Some(_)) => { - if !is_same_origin { - return Err(fdo::Error::AccessDenied(String::from( - "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 - // - if RP ID is set, but origin's effective domain doesn't match - // - query for related origins, if supported - // - fail if not supported, or if RP ID doesn't match any related origins. - let (get_cred_request, client_data_json) = - get_credential_request_try_into_ctap2(&request).map_err(|_| { - fdo::Error::Failed( - "Could not parse passkey assertion request.".to_owned(), - ) - })?; - let cred_request = - CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); - - let response = - execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; - - match response { - CredentialResponse::GetPublicKeyCredentialResponse(cred_response) => { - let public_key_response = get_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - )?; - Ok(public_key_response.into()) - } - _ => Err(fdo::Error::Failed( - "Invalid credential response received from authenticator".to_string(), - )), - } - } - _ => Err(fdo::Error::Failed( - "Unknown credential request type".to_string(), - )), - }; - response - } else { - tracing::info!("Window already open"); - Err(fdo::Error::ObjectPathInUse( - "WebAuthn session already open.".into(), - )) - } - } - - async fn get_client_capabilities(&self) -> fdo::Result { - Ok(GetClientCapabilitiesResponse { - conditional_create: false, - conditional_get: false, - hybrid_transport: true, - passkey_platform_authenticator: false, - user_verifying_platform_authenticator: false, - related_origins: false, - signal_all_accepted_credentials: false, - signal_current_user_details: false, - signal_unknown_credential: false, - }) - } -} - -pub async fn start_internal_service< - H: HybridHandler + Debug + Send + Sync + 'static, - U: UsbHandler + Debug + Send + Sync + 'static, - UC: UiController + Debug + Send + Sync + 'static, ->( - service_name: &str, - path: &str, - credential_service: CredentialService, -) -> zbus::Result { - connection::Builder::session()? - .name(service_name)? - .serve_at( - path, - InternalService { - signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), - svc: Arc::new(AsyncMutex::new(credential_service)), - usb_pin_tx: Arc::new(AsyncMutex::new(None)), - usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), - hybrid_event_forwarder_task: Arc::new(AsyncMutex::new(None)), - }, - )? - .build() - .await -} - -struct CredentialRequestController { - svc: Arc>>, -} - -#[interface(name = "xyz.iinuwa.credentials.impl.Credentials")] -impl CredentialRequestController -where - H: HybridHandler + Debug + Send + Sync + 'static, - U: UsbHandler + Debug + Send + Sync + 'static, - UC: UiController + Debug + Send + Sync + 'static, -{ - async fn create_credential( - &self, - request: CreateCredentialRequest, - ) -> fdo::Result { - match create_credential_request_try_into_ctap2(&request) { - Ok((make_request, client_data_json)) => { - let mut rx = { - let rx: Receiver> = self - .svc - .lock() - .await - .init_request(&CredentialRequest::CreatePublicKeyCredentialRequest( - make_request, - )) - .await; - rx - }; - let msg = rx.recv().await.ok_or_else(|| { - tracing::error!("Credential service shutdown response channel prematurely"); - fdo::Error::Failed("Credential service shutdown".to_string()) - })?; - match msg { - Ok(CredentialResponse::CreatePublicKeyCredentialResponse(cred_response)) => { - let public_key_response = create_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - )?; - Ok(public_key_response.into()) - } - // We should be returning the correct kind of response, so this shouldn't happen. - Ok(_) => Err(fdo::Error::Failed("Internal error occurred".to_string())), - Err(_) => Err(fdo::Error::Failed( - "Failed to create credential".to_string(), - )), - } - } - Err(_) => Err(fdo::Error::InvalidArgs( - "Unable to parse create credential request".to_string(), - )), - } - } - - async fn get_credential( - &self, - request: GetCredentialRequest, - ) -> fdo::Result { - match get_credential_request_try_into_ctap2(&request) { - Ok((get_request, client_data_json)) => { - let mut rx = { - let rx: Receiver> = self - .svc - .lock() - .await - .init_request(&CredentialRequest::GetPublicKeyCredentialRequest( - get_request, - )) - .await; - rx - }; - let msg = rx.recv().await.ok_or_else(|| { - tracing::error!("Credential service shutdown response channel prematurely"); - fdo::Error::Failed("Credential service shutdown".to_string()) - })?; - match msg { - Ok(CredentialResponse::GetPublicKeyCredentialResponse(cred_response)) => { - let public_key_response = get_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - )?; - Ok(public_key_response.into()) - } - // We should be returning the correct kind of response, so this shouldn't happen. - Ok(_) => Err(fdo::Error::Failed("Internal error occurred".to_string())), - Err(_) => Err(fdo::Error::Failed("Failed to get credential".to_string())), - } - } - Err(_) => Err(fdo::Error::InvalidArgs( - "Unable to parse get credential request".to_string(), - )), - } - } -} - -pub struct InternalService { - signal_state: Arc>, - svc: Arc>>, - usb_pin_tx: Arc>>>, - usb_event_forwarder_task: Arc>>, - hybrid_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. -#[interface( - name = "xyz.iinuwa.credentials.CredentialManagerInternal1", - proxy( - gen_blocking = false, - default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", - default_service = "xyz.iinuwa.credentials.CredentialManagerInternal", - ) -)] -impl InternalService -where - H: HybridHandler + Debug + Send + Sync + 'static, - U: UsbHandler + Debug + Send + Sync + 'static, - UC: UiController + Debug + Send + Sync + 'static, -{ - async fn initiate_event_stream( - &self, - #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, - ) -> fdo::Result<()> { - let mut signal_state = self.signal_state.lock().await; - match *signal_state { - SignalState::Idle => {} - SignalState::Pending(ref mut pending) => { - for msg in pending.iter_mut() { - emitter.state_changed(msg.clone()).await?; - } - } - SignalState::Active => {} - }; - *signal_state = SignalState::Active; - Ok(()) - } - - async fn get_available_public_key_devices(&self) -> fdo::Result> { - let devices = self - .svc - .lock() - .await - .get_available_public_key_devices() - .await - .map_err(|_| { - fdo::Error::Failed("Failed to get retrieve available devices".to_string()) - })?; - Ok(devices.into_iter().map(Device::from).collect()) - } - - async fn get_hybrid_credential( - &self, - #[zbus(object_server)] object_server: &ObjectServer, - ) -> fdo::Result<()> { - let svc = self.svc.lock().await; - let mut stream = svc.get_hybrid_credential(); - let signal_state = self.signal_state.clone(); - let object_server = object_server.clone(); - let task = tokio::spawn(async move { - let interface: zbus::Result>> = object_server - .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") - .await; - - let emitter = match interface { - Ok(ref i) => i.signal_emitter(), - Err(err) => { - tracing::error!("Failed to get connection to D-Bus to send signals: {err}"); - return; - } - }; - while let Some(state) = stream.next().await { - let event = - creds_lib::model::BackgroundEvent::HybridQrStateChanged(state.clone().into()) - .try_into(); - match event { - Err(err) => { - tracing::error!("Failed to serialize state update: {err}"); - break; - } - Ok(event) => match send_state_update(&emitter, &signal_state, event).await { - Ok(_) => {} - Err(err) => { - tracing::error!("Failed to send state update to UI: {err}"); - break; - } - }, - } - match state { - HybridState::Completed | HybridState::Failed => { - break; - } - _ => {} - }; - } - }) - .abort_handle(); - if let Some(prev_task) = self.hybrid_event_forwarder_task.lock().await.replace(task) { - prev_task.abort(); - } - Ok(()) - } - - async fn get_usb_credential( - &self, - #[zbus(object_server)] object_server: &ObjectServer, - ) -> fdo::Result<()> { - let mut stream = self.svc.lock().await.get_usb_credential(); - let usb_pin_tx = self.usb_pin_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>> = object_server - .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") - .await; - - let emitter = match interface { - Ok(ref i) => i.signal_emitter(), - Err(err) => { - tracing::error!("Failed to get connection to D-Bus to send signals: {err}"); - return; - } - }; - while let Some(state) = stream.next().await { - match creds_lib::model::BackgroundEvent::UsbStateChanged((&state).into()).try_into() - { - Err(err) => { - tracing::error!("Failed to serialize state update: {err}"); - break; - } - Ok(event) => match send_state_update(&emitter, &signal_state, event).await { - Ok(_) => {} - Err(err) => { - tracing::error!("Failed to send state update to UI: {err}"); - break; - } - }, - }; - match state { - UsbState::NeedsPin { pin_tx, .. } => { - let mut usb_pin_tx = usb_pin_tx.lock().await; - let _ = usb_pin_tx.insert(pin_tx); - } - UsbState::Completed | UsbState::Failed(_) => { - break; - } - _ => {} - }; - } - }) - .abort_handle(); - if let Some(prev_task) = self.usb_event_forwarder_task.lock().await.replace(task) { - prev_task.abort(); - } - Ok(()) - } - - async fn select_device(&self, device_id: String) -> fdo::Result<()> { - todo!() - } - - async fn enter_client_pin(&self, pin: String) -> fdo::Result<()> { - if let Some(pin_tx) = self.usb_pin_tx.lock().await.take() { - pin_tx.send(pin).await.unwrap(); - } - Ok(()) - } - - async fn select_credential(&self, credential_id: String) -> fdo::Result<()> { - todo!() - } - - #[zbus(signal)] - async fn state_changed( - emitter: &SignalEmitter<'_>, - update: BackgroundEvent, - ) -> zbus::Result<()>; -} -async fn send_state_update( - emitter: &SignalEmitter<'_>, - signal_state: &Arc>, - update: BackgroundEvent, -) -> fdo::Result<()> { - let mut signal_state = signal_state.lock().await; - match *signal_state { - SignalState::Idle => { - let pending = VecDeque::from([update]); - *signal_state = SignalState::Pending(pending); - } - SignalState::Pending(ref mut pending) => { - pending.push_back(update); - } - SignalState::Active => { - emitter.state_changed(update).await?; - } - }; - Ok(()) -} - -pub struct CredentialControlServiceClient { - conn: Connection, -} - -impl CredentialControlServiceClient { - pub fn new(conn: Connection) -> Self { - Self { conn } - } - - async fn proxy(&self) -> zbus::Result { - InternalServiceProxy::new(&self.conn).await - } -} - -impl CredentialManagementClient for CredentialControlServiceClient { - async fn init_request( - &self, - cred_request: CredentialRequest, - ) -> Receiver> { - // TODO: Start here - self.proxy().await.unwrap(). - } - - async fn complete_auth(&self) -> Result { - todo!() - } - - async fn get_available_public_key_devices( - &self, - ) -> Result, Box> { - let devices: Result, String> = self - .proxy() - .await? - .get_available_public_key_devices() - .await? - .into_iter() - .map(|d| d.try_into().map_err(|_| "Failed".to_string())) - .collect(); - Ok(devices?) - } - - async fn get_hybrid_credential(&mut self) -> Result<(), ()> { - todo!() - } - - async fn get_usb_credential(&mut self) -> Result<(), ()> { - todo!() - } - - async fn initiate_event_stream( - &mut self, - ) -> Result + Send + 'static>>, ()> - { - todo!() - } - - async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { - if let Err(err) = self.proxy().await.unwrap().enter_client_pin(pin).await { - tracing::error!("Failed to send client pin: {err}"); - return Err(()); - } - Ok(()) - } - - async fn select_credential(&self, credential_id: String) -> Result<(), ()> { - todo!() - } -} - -/// These methods are called by the credential service to control the UI. -#[proxy( - gen_blocking = false, - interface = "xyz.iinuwa.credentials.UiControl1", - default_service = "xyz.iinuwa.credentials.UiControl", - default_path = "/xyz/iinuwa/credentials/UiControl" -)] -// The #[proxy] macro renames this type to this creates a type UiControlServiceClientProxy -trait UiControlServiceClient { - fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; -} - -#[derive(Debug)] -pub struct UiControlServiceClient { - conn: Connection, -} -impl UiControlServiceClient { - pub fn new(conn: Connection) -> Self { - Self { conn } - } - - async fn proxy(&self) -> Result { - UiControlServiceClientProxy::new(&self.conn).await - } -} -impl UiController for UiControlServiceClient { - async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { - self.proxy() - .await? - .launch_ui(request) - .await - .map_err(|err| err.into()) - } -} - -async fn execute_flow( - // TODO: Replace this with UiControlClient - // gui_tx: &async_std::channel::Sender, - manager_client: &C, - cred_request: &CredentialRequest, -) -> zbus::Result { - let mut signal_rx = manager_client.init_request(cred_request.clone()).await; - let rsp = signal_rx - .recv() - .await - .ok_or(fdo::Error::Failed( - "Credential service unexpectedly interrupted".to_string(), - ))? - .map_err(|err| fdo::Error::Failed(err.to_string()))?; - Ok(rsp) - - /* - // start GUI - let operation = match &cred_request { - CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, - CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, - }; - let (signal_tx, signal_rx) = tokio::sync::oneshot::channel(); - let view_request = ViewRequest { - operation, - signal: signal_tx, - }; - // TODO: Replace this with a UiControlClient - // 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()) - }) - */ -} diff --git a/credsd/src/dbus/ui_control.rs b/credsd/src/dbus/ui_control.rs new file mode 100644 index 00000000..014a69d8 --- /dev/null +++ b/credsd/src/dbus/ui_control.rs @@ -0,0 +1,34 @@ +/// These methods are called by the credential service to control the UI. +#[proxy( + gen_blocking = false, + interface = "xyz.iinuwa.credentials.UiControl1", + default_service = "xyz.iinuwa.credentials.UiControl", + default_path = "/xyz/iinuwa/credentials/UiControl" +)] +// The #[proxy] macro renames this type to this creates a type UiControlServiceClientProxy +trait UiControlServiceClient { + fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; +} + +#[derive(Debug)] +pub struct UiControlServiceClient { + conn: Connection, +} +impl UiControlServiceClient { + pub fn new(conn: Connection) -> Self { + Self { conn } + } + + async fn proxy(&self) -> Result { + UiControlServiceClientProxy::new(&self.conn).await + } +} +impl UiController for UiControlServiceClient { + async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { + self.proxy() + .await? + .launch_ui(request) + .await + .map_err(|err| err.into()) + } +} From d86deb784cd54fa8fb7230c499bf936d5297b704 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 09:59:18 -0500 Subject: [PATCH 26/38] wip: Refactor gateway --- credsd/src/dbus/gateway.rs | 299 ++++++++++++++++++++++++++----------- 1 file changed, 212 insertions(+), 87 deletions(-) diff --git a/credsd/src/dbus/gateway.rs b/credsd/src/dbus/gateway.rs index 28f619ea..cca320a2 100644 --- a/credsd/src/dbus/gateway.rs +++ b/credsd/src/dbus/gateway.rs @@ -10,14 +10,37 @@ use creds_lib::{ GetCredentialResponse, }, }; +use serde::Deserialize; use tokio::sync::Mutex as AsyncMutex; -use zbus::{fdo, interface}; +use zbus::{fdo, interface, Connection, DBusError}; use crate::dbus::{ create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, - get_credential_request_try_into_ctap2, + get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, }; +pub const INTERFACE_NAME: &'static str = "xyz.iinuwa.credentials.Credentials1"; +pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.Credentials"; +pub const SERVICE_PATH: &'static str = "/xyz/iinuwa/credentials/Credentials"; + +pub async fn start_gateway( + controller: C, +) -> Result { + zbus::connection::Builder::session() + .inspect_err(|err| { + tracing::error!("Failed to connect to D-Bus session: {err}"); + })? + .name(SERVICE_NAME)? + .serve_at( + SERVICE_PATH, + CredentialGateway { + controller: Arc::new(AsyncMutex::new(controller)), + }, + )? + .build() + .await +} + struct CredentialGateway { controller: Arc>, } @@ -28,98 +51,119 @@ impl CredentialGateway fdo::Result { - if request.origin.is_none() { - todo!("Implicit caller-origin binding not yet implemented.") - }; - let is_same_origin = request.is_same_origin.unwrap_or(false); - let response = match (request.r#type.as_ref(), &request.public_key) { - ("publicKey", Some(_)) => { - if !is_same_origin { - return Err(fdo::Error::AccessDenied(String::from( - "Cross-origin public-key credentials are not allowed.", - ))); - } - let (make_cred_request, client_data_json) = - create_credential_request_try_into_ctap2(&request).map_err(|e| { - fdo::Error::Failed(format!( - "Could not parse passkey creation request: {e:?}" - )) - })?; - let cred_request = - CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); - - let response = execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; - - if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = - response - { - let public_key_response = create_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - )?; - Ok(public_key_response.into()) - } else { - Err(fdo::Error::Failed("Failed to create passkey".to_string())) - } + ) -> Result { + let (origin, is_same_origin, _top_origin) = check_origin( + request.origin.as_ref().map(|s| s.as_str()), + request.is_same_origin, + ) + .await + .map_err(Error::from)?; + if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { + if !is_same_origin { + // TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message. + // We should still reject cross-origin requests for conditionally-mediated requests. + tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); + return Err(WebAuthnError::NotAllowedError.into()); + } + let (make_cred_request, client_data_json) = + create_credential_request_try_into_ctap2(&request).map_err(|e| { + tracing::error!("Could not parse passkey creation request: {e:?}"); + WebAuthnError::TypeError + })?; + let cred_request = + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); + + let response = self + .controller + .lock() + .await + .request_credential(cred_request) + .await?; + + if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { + let public_key_response = + create_credential_response_try_from_ctap2(&cred_response, client_data_json) + .map_err(|err| { + tracing::error!( + "Failed to parse credential response from authenticator: {err}" + ); + // Using NotAllowedError as a catch-all error. + WebAuthnError::NotAllowedError + })?; + Ok(public_key_response.into()) + } else { + // TODO: is response safe to log here? + // tracing::error!("Expected create public key credential response, received {response:?}"); + tracing::error!("Did not receive expected create public key credential response."); + // Using NotAllowedError as a catch-all error. + Err(WebAuthnError::NotAllowedError.into()) } - _ => Err(fdo::Error::Failed( - "Unknown credential request type".to_string(), - )), - }; - response + } else { + tracing::error!("Unknown credential type request: {}", request.r#type); + Err(WebAuthnError::TypeError.into()) + } } async fn get_credential( &self, request: GetCredentialRequest, - ) -> fdo::Result { - if request.origin.is_none() { - todo!("Implicit caller-origin binding is not yet implemented."); - } - let is_same_origin = request.is_same_origin.unwrap_or(false); - let response = match (request.r#type.as_ref(), &request.public_key) { - ("publicKey", Some(_)) => { - if !is_same_origin { - return Err(fdo::Error::AccessDenied(String::from( - "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 - // - if RP ID is set, but origin's effective domain doesn't match - // - query for related origins, if supported - // - fail if not supported, or if RP ID doesn't match any related origins. - let (get_cred_request, client_data_json) = - get_credential_request_try_into_ctap2(&request).map_err(|_| { - fdo::Error::Failed("Could not parse passkey assertion request.".to_owned()) - })?; - let cred_request = - CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); - - let response = execute_flow(/* &tx, */ &self.manager_client, &cred_request).await?; - - match response { - CredentialResponse::GetPublicKeyCredentialResponse(cred_response) => { - let public_key_response = get_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - )?; - Ok(public_key_response.into()) - } - _ => Err(fdo::Error::Failed( - "Invalid credential response received from authenticator".to_string(), - )), - } + ) -> Result { + let (origin, is_same_origin, _top_origin) = check_origin( + request.origin.as_ref().map(|s| s.as_str()), + request.is_same_origin, + ) + .await + .map_err(Error::from)?; + if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { + if !is_same_origin { + // TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message. + tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); + return Err(WebAuthnError::NotAllowedError.into()); } - _ => Err(fdo::Error::Failed( - "Unknown credential request type".to_string(), - )), - }; - response + // 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 + // - if RP ID is set, but origin's effective domain doesn't match + // - query for related origins, if supported + // - fail if not supported, or if RP ID doesn't match any related origins. + let (get_cred_request, client_data_json) = + get_credential_request_try_into_ctap2(&request).map_err(|e| { + tracing::error!("Could not parse passkey assertion request: {e:?}"); + WebAuthnError::TypeError + })?; + let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); + + let response = self + .controller + .lock() + .await + .request_credential(cred_request) + .await?; + + if let CredentialResponse::GetPublicKeyCredentialResponse(cred_response) = response { + let public_key_response = + get_credential_response_try_from_ctap2(&cred_response, client_data_json) + .map_err(|err| { + tracing::error!( + "Failed to parse credential response from authenticator: {err}" + ); + // Using NotAllowedError as a catch-all error. + WebAuthnError::NotAllowedError + })?; + Ok(public_key_response.into()) + } else { + // TODO: is response safe to log here? + // tracing::error!("Expected get public key credential response, received {response:?}"); + tracing::error!("Did not receive expected get public key credential response."); + // Using NotAllowedError as a catch-all error. + Err(WebAuthnError::NotAllowedError.into()) + } + } else { + tracing::error!("Unknown credential type request: {}", request.r#type); + Err(WebAuthnError::TypeError.into()) + } } async fn get_client_capabilities(&self) -> fdo::Result { @@ -137,8 +181,89 @@ impl CredentialGateway, + is_same_origin: Option, + // TODO: Replace is_same_origin with explicit top_origin + // top_origin: Option<&str>, +) -> Result<(String, bool, String), WebAuthnError> { + let origin = if let Some(origin) = origin { + origin.to_string() + } else { + tracing::warn!( + "Caller requested implicit origin, which is not yet implemented. Rejecting request." + ); + return Err(WebAuthnError::SecurityError); + }; + let is_same_origin = is_same_origin.unwrap_or(false); + let top_origin = if is_same_origin { + origin.clone() + } else { + tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); + return Err(WebAuthnError::NotAllowedError.into()); + }; + Ok((origin, true, top_origin)) +} + trait CredentialRequestController { async fn request_credential( + &self, request: CredentialRequest, ) -> Result; } + +#[derive(DBusError, Debug)] +#[zbus(prefix = "xyz.iinuwa.credentials")] +enum Error { + #[zbus(error)] + ZBus(zbus::Error), + + /// The ceremony was cancelled by an AbortController. See § 5.6 Abort + /// Operations with AbortSignal and § 1.3.4 Aborting Authentication + /// Operations. + AbortError, + + /// Either `residentKey` was set to required and no available authenticator + /// supported resident keys, or `userVerification` was set to required and no + /// available authenticator could perform user verification. + ConstraintError, + + /// The authenticator used in the ceremony recognized an entry in + /// `excludeCredentials` after the user consented to registering a credential. + InvalidStateError, + + /// No entry in `pubKeyCredParams` had a type property of `public-key`, or the + /// authenticator did not support any of the signature algorithms specified + /// in `pubKeyCredParams`. + NotSupportedError, + + /// The effective domain was not a valid domain, or `rp.id` was not equal to + /// or a registrable domain suffix of the effective domain. In the latter + /// case, the client does not support related origin requests or the related + /// origins validation procedure failed. + SecurityError, + + /// A catch-all error covering a wide range of possible reasons, including + /// common ones like the user canceling out of the ceremony. Some of these + /// causes are documented throughout this spec, while others are + /// client-specific. + NotAllowedError, + + /// The options argument was not a valid `CredentialCreationOptions` value, or + /// the value of `user.id` was empty or was longer than 64 bytes. + TypeError, +} + +impl From for Error { + fn from(value: WebAuthnError) -> Self { + match value { + WebAuthnError::AbortError => Self::AbortError, + WebAuthnError::ConstraintError => Self::ConstraintError, + WebAuthnError::InvalidStateError => Self::InvalidStateError, + WebAuthnError::NotSupportedError => Self::NotSupportedError, + WebAuthnError::SecurityError => Self::SecurityError, + WebAuthnError::NotAllowedError => Self::NotAllowedError, + WebAuthnError::TypeError => Self::TypeError, + } + } +} From 3f4cc1d1d4e98719c0d63e0beb309a1dc024c459 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 09:59:18 -0500 Subject: [PATCH 27/38] wip: Refactor flow_control --- creds-ui/src/client.rs | 6 +- creds-ui/src/dbus.rs | 8 +- credsd/src/credential_service/mod.rs | 23 +++-- credsd/src/credential_service/server.rs | 8 +- credsd/src/dbus/broker.rs | 25 ----- credsd/src/dbus/flow_control.rs | 123 +++++++++++++++++++++++- credsd/src/dbus/gateway.rs | 8 +- credsd/src/dbus/mod.rs | 16 ++- credsd/src/main.rs | 25 +++-- 9 files changed, 167 insertions(+), 75 deletions(-) diff --git a/creds-ui/src/client.rs b/creds-ui/src/client.rs index a225cc0a..d9b690cd 100644 --- a/creds-ui/src/client.rs +++ b/creds-ui/src/client.rs @@ -3,7 +3,7 @@ use creds_lib::client::CredentialServiceClient; use futures_lite::StreamExt; use zbus::{Connection, zvariant}; -use crate::dbus::InternalServiceProxy; +use crate::dbus::FlowControlServiceProxy; pub struct DbusCredentialClient { conn: Connection, @@ -13,8 +13,8 @@ impl DbusCredentialClient { pub fn new(conn: Connection) -> Self { Self { conn } } - async fn proxy(&self) -> std::result::Result { - InternalServiceProxy::new(&self.conn) + async fn proxy(&self) -> std::result::Result { + FlowControlServiceProxy::new(&self.conn) .await .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) } diff --git a/creds-ui/src/dbus.rs b/creds-ui/src/dbus.rs index c755e75f..18bbe3aa 100644 --- a/creds-ui/src/dbus.rs +++ b/creds-ui/src/dbus.rs @@ -4,11 +4,11 @@ use zbus::{fdo, interface, proxy}; #[proxy( gen_blocking = false, - interface = "xyz.iinuwa.credentials.CredentialManagerInternal1", - default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", - default_service = "xyz.iinuwa.credentials.CredentialManagerInternal" + interface = "xyz.iinuwa.credentials.FlowControl1", + default_path = "/xyz/iinuwa/credentials/FlowControl", + default_service = "xyz.iinuwa.credentials.FlowControl" )] -pub trait InternalService { +pub trait FlowControlService { async fn initiate_event_stream(&self) -> fdo::Result<()>; async fn get_available_public_key_devices(&self) -> fdo::Result>; diff --git a/credsd/src/credential_service/mod.rs b/credsd/src/credential_service/mod.rs index 832827e2..baabb9cc 100644 --- a/credsd/src/credential_service/mod.rs +++ b/credsd/src/credential_service/mod.rs @@ -86,8 +86,9 @@ impl pub async fn init_request( &self, request: &CredentialRequest, - ) -> Receiver> { - let (tx, rx) = mpsc::channel(1); + tx: Sender>, + ) { + // let (tx, rx) = mpsc::channel(1); let res = { let mut cred_request = self.cred_request.lock().unwrap(); if cred_request.is_some() { @@ -104,7 +105,7 @@ impl ))) .await .expect("Send to local receiver to succeed"); - return rx; + return; } let operation = match &request { CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, @@ -123,7 +124,6 @@ impl let err = Err(CredentialServiceError::Internal(err)); tx.send(err).await; } - return rx; } pub async fn get_available_public_key_devices(&self) -> Result, ()> { @@ -153,10 +153,17 @@ impl }) } - pub fn complete_auth(&self) -> Option { - self.cred_request.lock().unwrap().take(); - let mut cred_response = self.cred_response.lock().unwrap(); - cred_response.take() + pub async fn complete_auth( + &self, + response: Result, + ) -> () { + if let Some((_request, responder)) = self.cred_request.lock().unwrap().take() { + if responder.send(response).await.is_err() { + tracing::error!("Failed to send response to back to caller"); + }; + } else { + tracing::error!("No corresponding request for this"); + } } } diff --git a/credsd/src/credential_service/server.rs b/credsd/src/credential_service/server.rs index d3c7343e..0623b34a 100644 --- a/credsd/src/credential_service/server.rs +++ b/credsd/src/credential_service/server.rs @@ -195,15 +195,21 @@ impl< &self, cred_request: CredentialRequest, ) -> Receiver> { - self.svc.lock().await.init_request(&cred_request).await + // self.svc.lock().await.init_request(&cred_request).await + todo!() } async fn complete_auth(&self) -> Result { + /* self.svc .lock() .await .complete_auth() + .await .ok_or("No credentials in credential service".to_string()) + */ + + todo!() } async fn get_available_public_key_devices(&self) -> Result, Box> { diff --git a/credsd/src/dbus/broker.rs b/credsd/src/dbus/broker.rs index eb01576d..ff4c287a 100644 --- a/credsd/src/dbus/broker.rs +++ b/credsd/src/dbus/broker.rs @@ -17,31 +17,6 @@ pub(crate) async fn start_service( - service_name: &str, - path: &str, - credential_service: CredentialService, -) -> zbus::Result { - connection::Builder::session()? - .name(service_name)? - .serve_at( - path, - InternalService { - signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), - svc: Arc::new(AsyncMutex::new(credential_service)), - usb_pin_tx: Arc::new(AsyncMutex::new(None)), - usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), - hybrid_event_forwarder_task: Arc::new(AsyncMutex::new(None)), - }, - )? - .build() - .await -} - struct CredentialRequestController { svc: Arc>>, } diff --git a/credsd/src/dbus/flow_control.rs b/credsd/src/dbus/flow_control.rs index afcc524f..d48aefd9 100644 --- a/credsd/src/dbus/flow_control.rs +++ b/credsd/src/dbus/flow_control.rs @@ -1,4 +1,74 @@ -pub struct InternalService { +use std::future::Future; +use std::{collections::VecDeque, error::Error, fmt::Debug, pin::Pin, sync::Arc}; + +use creds_lib::model::{ + CredentialRequest, CredentialResponse, Error as CredentialServiceError, WebAuthnError, +}; +use creds_lib::server::{BackgroundEvent, Device}; +use futures_lite::{Stream, StreamExt}; +use tokio::{ + sync::{ + mpsc::{self, Receiver, Sender}, + Mutex as AsyncMutex, + }, + task::AbortHandle, +}; +use zbus::{ + connection::{Builder, Connection}, + fdo, interface, + object_server::{InterfaceRef, SignalEmitter}, + ObjectServer, +}; + +use crate::credential_service::{ + hybrid::{HybridHandler, HybridState}, + usb::UsbHandler, + CredentialManagementClient, CredentialService, UiController, UsbState, +}; + +pub const SERVICE_PATH: &'static str = "/xyz/iinuwa/credentials/FlowControl"; +pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.FlowControl"; + +pub async fn start_flow_control_service< + H: HybridHandler + Debug + Send + Sync + 'static, + U: UsbHandler + Debug + Send + Sync + 'static, + UC: UiController + Debug + Send + Sync + 'static, +>( + credential_service: CredentialService, +) -> zbus::Result<( + Connection, + Sender<( + CredentialRequest, + Sender>, + )>, +)> { + let svc = Arc::new(AsyncMutex::new(credential_service)); + let svc2 = svc.clone(); + let conn = Builder::session()? + .name(SERVICE_NAME)? + .serve_at( + SERVICE_PATH, + InternalService { + signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), + svc, + usb_pin_tx: Arc::new(AsyncMutex::new(None)), + usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), + hybrid_event_forwarder_task: Arc::new(AsyncMutex::new(None)), + }, + )? + .build() + .await?; + let (initiator_tx, mut initiator_rx) = mpsc::channel(2); + tokio::spawn(async move { + let svc = svc2; + while let Some((msg, tx)) = initiator_rx.recv().await { + svc.lock().await.init_request(&msg, tx).await; + } + }); + Ok((conn, initiator_tx)) +} + +struct InternalService { signal_state: Arc>, svc: Arc>>, usb_pin_tx: Arc>>>, @@ -10,11 +80,11 @@ pub struct InternalService { /// UI and the credential service, and should not be called by arbitrary /// clients. #[interface( - name = "xyz.iinuwa.credentials.CredentialManagerInternal1", + name = "xyz.iinuwa.credentials.FlowControl1", proxy( gen_blocking = false, - default_path = "/xyz/iinuwa/credentials/CredentialManagerInternal", - default_service = "xyz.iinuwa.credentials.CredentialManagerInternal", + default_path = "/xyz/iinuwa/credentials/FlowControl", + default_service = "xyz.iinuwa.credentials.FlowControl", ) )] impl InternalService @@ -202,6 +272,16 @@ async fn send_state_update( Ok(()) } +enum SignalState { + /// No state + Idle, + /// Waiting for client to signal that it's ready to receive events. + /// Holds a cache of events to send once the client connects. + Pending(VecDeque), + /// Client is actively receiving messages. + Active, +} + pub struct CredentialControlServiceClient { conn: Connection, } @@ -222,7 +302,8 @@ impl CredentialManagementClient for CredentialControlServiceClient { cred_request: CredentialRequest, ) -> Receiver> { // TODO: Start here - self.proxy().await.unwrap(). + // self.proxy().await.unwrap(). + todo!() } async fn complete_auth(&self) -> Result { @@ -270,3 +351,35 @@ impl CredentialManagementClient for CredentialControlServiceClient { todo!() } } + +pub trait CredentialRequestController { + fn request_credential( + &self, + request: CredentialRequest, + ) -> impl Future> + Send; +} + +pub struct CredentialRequestControllerClient { + pub initiator: Sender<( + CredentialRequest, + Sender>, + )>, +} + +impl CredentialRequestController for CredentialRequestControllerClient { + async fn request_credential( + &self, + request: CredentialRequest, + ) -> Result { + let (tx, mut rx) = mpsc::channel(4); + // TODO: We need a PlatformError variant. + self.initiator.send((request, tx)).await.unwrap(); + if let Some(msg) = rx.recv().await { + // TODO: Pass real WebAuthnError from credential service + msg.map_err(|_| WebAuthnError::NotAllowedError) + } else { + // if the sender was dropped, then the operation is cancelled. + Err(WebAuthnError::NotAllowedError) + } + } +} diff --git a/credsd/src/dbus/gateway.rs b/credsd/src/dbus/gateway.rs index cca320a2..7721fe3e 100644 --- a/credsd/src/dbus/gateway.rs +++ b/credsd/src/dbus/gateway.rs @@ -17,6 +17,7 @@ use zbus::{fdo, interface, Connection, DBusError}; use crate::dbus::{ create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, + CredentialRequestController, }; pub const INTERFACE_NAME: &'static str = "xyz.iinuwa.credentials.Credentials1"; @@ -205,13 +206,6 @@ async fn check_origin( Ok((origin, true, top_origin)) } -trait CredentialRequestController { - async fn request_credential( - &self, - request: CredentialRequest, - ) -> Result; -} - #[derive(DBusError, Debug)] #[zbus(prefix = "xyz.iinuwa.credentials")] enum Error { diff --git a/credsd/src/dbus/mod.rs b/credsd/src/dbus/mod.rs index 4d51a37b..af3d791d 100644 --- a/credsd/src/dbus/mod.rs +++ b/credsd/src/dbus/mod.rs @@ -73,12 +73,10 @@ use crate::credential_service::{ CredentialManagementClient, CredentialService, UiController, UsbState, }; -enum SignalState { - /// No state - Idle, - /// Waiting for client to signal that it's ready to receive events. - /// Holds a cache of events to send once the client connects. - Pending(VecDeque), - /// Client is actively receiving messages. - Active, -} +pub use self::{ + flow_control::{ + start_flow_control_service, CredentialRequestController, CredentialRequestControllerClient, + SERVICE_NAME as FLOW_CONTROL_SERVICE_NAME, SERVICE_PATH as FLOW_CONTROL_SERVICE_PATH, + }, + gateway::start_gateway, +}; diff --git a/credsd/src/main.rs b/credsd/src/main.rs index 1d62a453..daa37cfb 100644 --- a/credsd/src/main.rs +++ b/credsd/src/main.rs @@ -12,7 +12,9 @@ use crate::{ credential_service::{ hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, InProcessServer, }, - dbus::{CredentialControlServiceClient, UiControlServiceClient}, + dbus::{ + CredentialControlServiceClient, CredentialRequestControllerClient, UiControlServiceClient, + }, }; #[tokio::main] @@ -32,13 +34,6 @@ async fn run() -> Result<(), Box> { let dbus_client_conn = zbus::connection::Builder::session()?.build().await?; println!(" ✅"); - print!("Starting D-Bus public client service..."); - let service_name = "xyz.iinuwa.credentials.Credentials"; - let path = "/xyz/iinuwa/credentials/Credentials"; - let cred_mgr = CredentialControlServiceClient::new(dbus_client_conn.clone()); - let _conn = dbus::start_service(service_name, path, cred_mgr).await?; - println!(" ✅"); - print!("Starting D-Bus UI -> Credential control service..."); let ui_controller = UiControlServiceClient::new(dbus_client_conn); let credential_service = CredentialService::new( @@ -46,11 +41,15 @@ async fn run() -> Result<(), Box> { InProcessUsbHandler {}, ui_controller, ); - let internal_service_name = "xyz.iinuwa.credentials.CredentialManagerInternal"; - let internal_path = "/xyz/iinuwa/credentials/CredentialManagerInternal"; - let _internal_service = - dbus::start_internal_service(internal_service_name, internal_path, credential_service) - .await?; + let (_flow_control_conn, initiator) = + dbus::start_flow_control_service(credential_service).await?; + println!(" ✅"); + + print!("Starting D-Bus public client service..."); + let service_name = "xyz.iinuwa.credentials.Credentials"; + let path = "/xyz/iinuwa/credentials/Credentials"; + let initiator = CredentialRequestControllerClient { initiator }; + let _gateway_conn = dbus::start_gateway(initiator).await?; println!(" ✅"); println!("Waiting for messages..."); From 70575360cae0ec8b5d9fe0014ae50775bb27cc7f Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 13:16:52 -0500 Subject: [PATCH 28/38] wip: Refactor ui_control --- credsd/src/dbus/mod.rs | 1 + credsd/src/dbus/ui_control.rs | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/credsd/src/dbus/mod.rs b/credsd/src/dbus/mod.rs index af3d791d..f895e8bf 100644 --- a/credsd/src/dbus/mod.rs +++ b/credsd/src/dbus/mod.rs @@ -79,4 +79,5 @@ pub use self::{ SERVICE_NAME as FLOW_CONTROL_SERVICE_NAME, SERVICE_PATH as FLOW_CONTROL_SERVICE_PATH, }, gateway::start_gateway, + ui_control::UiControlServiceClient, }; diff --git a/credsd/src/dbus/ui_control.rs b/credsd/src/dbus/ui_control.rs index 014a69d8..ccbfd8ad 100644 --- a/credsd/src/dbus/ui_control.rs +++ b/credsd/src/dbus/ui_control.rs @@ -1,12 +1,19 @@ -/// These methods are called by the credential service to control the UI. +//! These methods are called by the credential service to control the UI. + +use std::error::Error; + +use zbus::{fdo, proxy, Connection}; + +use crate::credential_service::UiController; +use creds_lib::server::ViewRequest; + #[proxy( gen_blocking = false, interface = "xyz.iinuwa.credentials.UiControl1", default_service = "xyz.iinuwa.credentials.UiControl", default_path = "/xyz/iinuwa/credentials/UiControl" )] -// The #[proxy] macro renames this type to this creates a type UiControlServiceClientProxy -trait UiControlServiceClient { +trait UiControlService { fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; } @@ -14,13 +21,14 @@ trait UiControlServiceClient { pub struct UiControlServiceClient { conn: Connection, } + impl UiControlServiceClient { pub fn new(conn: Connection) -> Self { Self { conn } } - async fn proxy(&self) -> Result { - UiControlServiceClientProxy::new(&self.conn).await + async fn proxy(&self) -> Result { + UiControlServiceProxy::new(&self.conn).await } } impl UiController for UiControlServiceClient { From 9fbdcec0fae3ceaeb43fc93c37c900b68f22e0c6 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 13:16:52 -0500 Subject: [PATCH 29/38] Clean up --- .vscode/launch.json | 2 +- creds-ui/src/main.rs | 5 ++++- credsd/src/dbus/flow_control.rs | 14 ++++++++------ credsd/src/dbus/mod.rs | 2 +- credsd/src/main.rs | 15 +++++++-------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 70f665b6..ae558d6b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,8 +24,8 @@ "program": "${workspaceFolder}/build/creds-ui/src/creds-ui", "args": [], "env": { - "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/xyz-iinuwa-credential-manager-portal-gtk/data", "RUST_LOG": "creds_ui=debug,zbus::trace" + "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/creds-ui/data", }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", diff --git a/creds-ui/src/main.rs b/creds-ui/src/main.rs index 9fe6ad34..6d7cea20 100644 --- a/creds-ui/src/main.rs +++ b/creds-ui/src/main.rs @@ -19,7 +19,10 @@ 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 conn = zbus::connection::Builder::session()?.build().await?; + let conn = zbus::connection::Builder::session()? + .name("xyz.iinuwa.credentials.UiControl")? + .build() + .await?; let cred_client = DbusCredentialClient::new(conn); let _handle = gui::start_gui_thread(request_rx, cred_client)?; println!(" ✅"); diff --git a/credsd/src/dbus/flow_control.rs b/credsd/src/dbus/flow_control.rs index d48aefd9..f036ee32 100644 --- a/credsd/src/dbus/flow_control.rs +++ b/credsd/src/dbus/flow_control.rs @@ -25,7 +25,7 @@ use crate::credential_service::{ usb::UsbHandler, CredentialManagementClient, CredentialService, UiController, UsbState, }; - +pub const INTERFACE_NAME: &'static str = "xyz.iinuwa.credentials.FlowControl1"; pub const SERVICE_PATH: &'static str = "/xyz/iinuwa/credentials/FlowControl"; pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.FlowControl"; @@ -48,7 +48,7 @@ pub async fn start_flow_control_service< .name(SERVICE_NAME)? .serve_at( SERVICE_PATH, - InternalService { + FlowControlService { signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), svc, usb_pin_tx: Arc::new(AsyncMutex::new(None)), @@ -68,7 +68,7 @@ pub async fn start_flow_control_service< Ok((conn, initiator_tx)) } -struct InternalService { +struct FlowControlService { signal_state: Arc>, svc: Arc>>, usb_pin_tx: Arc>>>, @@ -87,7 +87,7 @@ struct InternalService { default_service = "xyz.iinuwa.credentials.FlowControl", ) )] -impl InternalService +impl FlowControlService where H: HybridHandler + Debug + Send + Sync + 'static, U: UsbHandler + Debug + Send + Sync + 'static, @@ -291,11 +291,12 @@ impl CredentialControlServiceClient { Self { conn } } - async fn proxy(&self) -> zbus::Result { - InternalServiceProxy::new(&self.conn).await + async fn proxy(&self) -> zbus::Result { + FlowControlServiceProxy::new(&self.conn).await } } +/* impl CredentialManagementClient for CredentialControlServiceClient { async fn init_request( &self, @@ -351,6 +352,7 @@ impl CredentialManagementClient for CredentialControlServiceClient { todo!() } } + */ pub trait CredentialRequestController { fn request_credential( diff --git a/credsd/src/dbus/mod.rs b/credsd/src/dbus/mod.rs index f895e8bf..e9912531 100644 --- a/credsd/src/dbus/mod.rs +++ b/credsd/src/dbus/mod.rs @@ -29,7 +29,7 @@ //! - launch UI //! - send_state_changed() -mod broker; +// mod broker; mod flow_control; mod gateway; mod model; diff --git a/credsd/src/main.rs b/credsd/src/main.rs index daa37cfb..c6f4b956 100644 --- a/credsd/src/main.rs +++ b/credsd/src/main.rs @@ -6,15 +6,13 @@ mod dbus; mod serde; mod webauthn; -use std::{error::Error, sync::Arc}; +use std::error::Error; use crate::{ credential_service::{ - hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, InProcessServer, - }, - dbus::{ - CredentialControlServiceClient, CredentialRequestControllerClient, UiControlServiceClient, + hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, }, + dbus::{CredentialRequestControllerClient, UiControlServiceClient}, }; #[tokio::main] @@ -31,7 +29,10 @@ async fn main() { async fn run() -> Result<(), Box> { print!("Connecting to D-Bus as client...\t"); - let dbus_client_conn = zbus::connection::Builder::session()?.build().await?; + let dbus_client_conn = zbus::connection::Builder::session()? + .name("xyz.iinuwa.credentials.Credsd")? + .build() + .await?; println!(" ✅"); print!("Starting D-Bus UI -> Credential control service..."); @@ -46,8 +47,6 @@ async fn run() -> Result<(), Box> { println!(" ✅"); print!("Starting D-Bus public client service..."); - let service_name = "xyz.iinuwa.credentials.Credentials"; - let path = "/xyz/iinuwa/credentials/Credentials"; let initiator = CredentialRequestControllerClient { initiator }; let _gateway_conn = dbus::start_gateway(initiator).await?; println!(" ✅"); From ae49ac401f624cc18d86da68fcbffc7133619468 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 13:16:52 -0500 Subject: [PATCH 30/38] Make debugging in VS Code easier Add zbus debug output; allow launching server and client simultaneously. --- .vscode/launch.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ae558d6b..f7954062 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "program": "${workspaceFolder}/build/credsd/src/credsd", "args": [], "env": { - "RUST_LOG": "credsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug" + "RUST_LOG": "credsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug" }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", @@ -24,12 +24,18 @@ "program": "${workspaceFolder}/build/creds-ui/src/creds-ui", "args": [], "env": { - "RUST_LOG": "creds_ui=debug,zbus::trace" "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/creds-ui/data", + "RUST_LOG": "creds_ui=debug,zbus::trace,zbus::object_server::debug" }, "sourceLanguages": ["rust"], "cwd": "${workspaceFolder}", "preLaunchTask": "Meson: Build UI" }, + ], + "compounds": [ + { + "name": "Server/Client", + "configurations": ["Debug UI (creds-ui)", "Debug Daemon (credsd)"] + } ] } From 6e36f948ec4fdaddba96e1f028dd0b3ebd275b89 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 13:16:52 -0500 Subject: [PATCH 31/38] Call correct proxy method for USB flow --- creds-ui/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/creds-ui/src/client.rs b/creds-ui/src/client.rs index d9b690cd..78161cf6 100644 --- a/creds-ui/src/client.rs +++ b/creds-ui/src/client.rs @@ -45,7 +45,7 @@ impl CredentialServiceClient for DbusCredentialClient { async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { self.proxy() .await? - .get_hybrid_credential() + .get_usb_credential() .await .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) .map_err(|_| ()) From deded304f580afc01b2435ef3cf450b115cb13ba Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 13:16:52 -0500 Subject: [PATCH 32/38] Fix D-Bus deserialization for USB/Hybrid state --- creds-lib/src/server.rs | 99 +++++++++++++++++++++++---------- creds-ui/src/client.rs | 4 +- credsd/Cargo.toml | 2 +- credsd/src/dbus/flow_control.rs | 18 +++--- 4 files changed, 81 insertions(+), 42 deletions(-) diff --git a/creds-lib/src/server.rs b/creds-lib/src/server.rs index f876d102..643c9f34 100644 --- a/creds-lib/src/server.rs +++ b/creds-lib/src/server.rs @@ -1,12 +1,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use zbus::{ - fdo, - object_server::SignalEmitter, - proxy, - zvariant::{self, DeserializeDict, LE, Optional, OwnedValue, SerializeDict, Type, Value}, -}; +use zbus::zvariant::{self, DeserializeDict, LE, Optional, OwnedValue, SerializeDict, Type, Value}; use crate::model::{Operation, ViewUpdate}; @@ -225,9 +220,10 @@ impl TryFrom> for Credential { */ #[derive(SerializeDict, DeserializeDict, Type)] +#[zvariant(signature = "a{sv}")] pub struct Device { - id: String, - transport: String, + pub id: String, + pub transport: String, } impl TryFrom> for Device { @@ -330,13 +326,17 @@ impl From for HybridState { match value { crate::model::HybridState::Idle => HybridState::Idle(OwnedValue::from(false)), crate::model::HybridState::Started(qr_code) => { - HybridState::Idle(value_to_owned(Value::from(qr_code))) + HybridState::Started(value_to_owned(&Value::from(qr_code))) + } + crate::model::HybridState::Connecting => { + HybridState::Connecting(OwnedValue::from(false)) } - crate::model::HybridState::Connecting => HybridState::Idle(OwnedValue::from(false)), - crate::model::HybridState::Connected => HybridState::Idle(OwnedValue::from(false)), - crate::model::HybridState::Completed => HybridState::Idle(OwnedValue::from(false)), - crate::model::HybridState::UserCancelled => HybridState::Idle(OwnedValue::from(false)), - crate::model::HybridState::Failed => HybridState::Idle(OwnedValue::from(false)), + crate::model::HybridState::Connected => HybridState::Connected(OwnedValue::from(false)), + crate::model::HybridState::Completed => HybridState::Completed(OwnedValue::from(false)), + crate::model::HybridState::UserCancelled => { + HybridState::UserCancelled(OwnedValue::from(false)) + } + crate::model::HybridState::Failed => HybridState::Failed(OwnedValue::from(false)), } } } @@ -358,10 +358,28 @@ impl TryFrom for crate::model::HybridState { impl TryFrom> for HybridState { type Error = zvariant::Error; fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zvariant::to_bytes(ctx, &value)?; - let obj: Self = encoded.deserialize()?.0; - Ok(obj) + let fields: HashMap> = value.try_into()?; + let tag = fields + .get("type") + .ok_or(zvariant::Error::Message( + "Expected a dictionary with `type` key".to_string(), + )) + .and_then(|t| t.try_into())?; + let value = fields.get("value").ok_or(zvariant::Error::Message( + "Expected a dictionary with `value` key".to_string(), + ))?; + match tag { + "IDLE" => Ok(Self::Idle(value_to_owned(value))), + "STARTED" => Ok(Self::Started(value_to_owned(value))), + "CONNECTING" => Ok(Self::Connecting(value_to_owned(value))), + "CONNECTED" => Ok(Self::Connected(value_to_owned(value))), + "COMPLETED" => Ok(Self::Completed(value_to_owned(value))), + "USER_CANCELLED" => Ok(Self::Completed(value_to_owned(value))), + "FAILED" => Ok(Self::Failed(value_to_owned(value))), + _ => Err(zvariant::Error::Message(format!( + "Invalid HybridState type passed: {tag}" + ))), + } } } @@ -370,31 +388,31 @@ impl From for Value<'_> { let mut fields = HashMap::new(); match value { HybridState::Idle(owned_value) => { - fields.insert("type", value_to_owned(Value::from("IDLE"))); + fields.insert("type", value_to_owned(&Value::from("IDLE"))); fields.insert("value", owned_value); } HybridState::Started(owned_value) => { - fields.insert("type", value_to_owned(Value::from("STARTED"))); + fields.insert("type", value_to_owned(&Value::from("STARTED"))); fields.insert("value", owned_value); } HybridState::Connecting(owned_value) => { - fields.insert("type", value_to_owned(Value::from("CONNECTING"))); + fields.insert("type", value_to_owned(&Value::from("CONNECTING"))); fields.insert("value", owned_value); } HybridState::Connected(owned_value) => { - fields.insert("type", value_to_owned(Value::from("CONNECTED"))); + fields.insert("type", value_to_owned(&Value::from("CONNECTED"))); fields.insert("value", owned_value); } HybridState::Completed(owned_value) => { - fields.insert("type", value_to_owned(Value::from("COMPLETED"))); + fields.insert("type", value_to_owned(&Value::from("COMPLETED"))); fields.insert("value", owned_value); } HybridState::UserCancelled(owned_value) => { - fields.insert("type", value_to_owned(Value::from("USER_CANCELLED"))); + fields.insert("type", value_to_owned(&Value::from("USER_CANCELLED"))); fields.insert("value", owned_value); } HybridState::Failed(owned_value) => { - fields.insert("type", value_to_owned(Value::from("FAILED"))); + fields.insert("type", value_to_owned(&Value::from("FAILED"))); fields.insert("value", owned_value); } } @@ -561,10 +579,31 @@ impl From for UsbState { impl TryFrom> for UsbState { type Error = zvariant::Error; fn try_from(value: Value<'_>) -> std::result::Result { - let ctx = zvariant::serialized::Context::new_dbus(LE, 0); - let encoded = zvariant::to_bytes(ctx, &value)?; - let obj: Self = encoded.deserialize()?.0; - Ok(obj) + let fields: HashMap> = value.try_into()?; + let tag = fields + .get("type") + .ok_or(zvariant::Error::Message( + "Expected a dictionary with `type` key".to_string(), + )) + .and_then(|t| t.try_into())?; + let value = fields.get("value").ok_or(zvariant::Error::Message( + "Expected a dictionary with `value` key".to_string(), + ))?; + match tag { + "IDLE" => Ok(Self::Idle(value_to_owned(value))), + "WAITING" => Ok(Self::Waiting(value_to_owned(value))), + "SELECTING_DEVICE" => Ok(Self::SelectingDevice(value_to_owned(value))), + "CONNECTED" => Ok(Self::SelectingDevice(value_to_owned(value))), + "NEEDS_PIN" => Ok(Self::NeedsPin(value_to_owned(value))), + "NEEDS_USER_VERIFICATION" => Ok(Self::NeedsUserVerification(value_to_owned(value))), + "NEEDS_USER_PRESENCE" => Ok(Self::NeedsUserPresence(value_to_owned(value))), + "SELECT_CREDENTIAL" => Ok(Self::SelectCredential(value_to_owned(value))), + "COMPLETED" => Ok(Self::Completed(value_to_owned(value))), + "FAILED" => Ok(Self::Failed(value_to_owned(value))), + _ => Err(zvariant::Error::Message(format!( + "Invalid UsbState type passed: {tag}" + ))), + } } } @@ -672,7 +711,7 @@ pub struct ViewRequest { pub operation: Operation, } -fn value_to_owned(value: Value<'_>) -> OwnedValue { +fn value_to_owned(value: &Value<'_>) -> OwnedValue { value .try_to_owned() .expect("non-file descriptor values to succeed") diff --git a/creds-ui/src/client.rs b/creds-ui/src/client.rs index 78161cf6..c5c6d9e1 100644 --- a/creds-ui/src/client.rs +++ b/creds-ui/src/client.rs @@ -29,7 +29,9 @@ impl CredentialServiceClient for DbusCredentialClient { .await? .get_available_public_key_devices() .await - .map_err(|_| ())?; + .map_err(|err| { + tracing::error!("Failed to retrieve available devices/transports: {err}") + })?; dbus_devices.into_iter().map(|d| d.try_into()).collect() } diff --git a/credsd/Cargo.toml b/credsd/Cargo.toml index 731c2885..39962f03 100644 --- a/credsd/Cargo.toml +++ b/credsd/Cargo.toml @@ -17,7 +17,7 @@ serde_json = "1.0.140" # serde_cbor = "0.11.1" tracing = "0.1.41" tracing-subscriber = "0.3" -zbus = { version = "5.5.0", default-features = false, features = ["blocking-api", "tokio"] } +zbus = { version = "5.9.0", default-features = false, features = ["tokio"] } libwebauthn = "~0.2.2" async-trait = "0.1.88" tokio = { version = "1.45.0", features = ["rt-multi-thread"] } diff --git a/credsd/src/dbus/flow_control.rs b/credsd/src/dbus/flow_control.rs index f036ee32..db88349f 100644 --- a/credsd/src/dbus/flow_control.rs +++ b/credsd/src/dbus/flow_control.rs @@ -118,10 +118,10 @@ where .await .get_available_public_key_devices() .await - .map_err(|_| { - fdo::Error::Failed("Failed to get retrieve available devices".to_string()) - })?; - Ok(devices.into_iter().map(Device::from).collect()) + .map_err(|_| fdo::Error::Failed("Failed to retrieve available devices".to_string()))?; + let dbus_devices: Vec = devices.into_iter().map(Device::from).collect(); + + Ok(dbus_devices) } async fn get_hybrid_credential( @@ -133,9 +133,8 @@ where let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: zbus::Result>> = object_server - .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") - .await; + let interface: zbus::Result>> = + object_server.interface(SERVICE_PATH).await; let emitter = match interface { Ok(ref i) => i.signal_emitter(), @@ -185,9 +184,8 @@ where let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { - let interface: zbus::Result>> = object_server - .interface("/xyz/iinuwa/credentials/CredentialManagerInternal") - .await; + let interface: zbus::Result>> = + object_server.interface(SERVICE_PATH).await; let emitter = match interface { Ok(ref i) => i.signal_emitter(), From f0784a8ee18d5f977b36ebcc9c9c4b5dc740e078 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 20:31:56 -0500 Subject: [PATCH 33/38] Fix selection of multiple credentials --- creds-ui/src/gui/view_model/mod.rs | 17 +++++++++++++---- credsd/src/dbus/flow_control.rs | 12 +++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/creds-ui/src/gui/view_model/mod.rs b/creds-ui/src/gui/view_model/mod.rs index 7b6499e2..3454baa0 100644 --- a/creds-ui/src/gui/view_model/mod.rs +++ b/creds-ui/src/gui/view_model/mod.rs @@ -164,10 +164,19 @@ impl ViewModel { cred_id, self.selected_device ); - if let Some(cred_tx) = self.usb_cred_tx.take() { - if cred_tx.lock().await.send(cred_id.clone()).await.is_err() { - error!("Failed to send selected credential to device"); - } + if let Err(_) = self + .credential_service + .lock() + .await + .select_credential(cred_id) + .await + { + let error_msg = "Failed to select credential from device."; + tracing::error!(error_msg); + self.tx_update + .send(ViewUpdate::Failed(error_msg.to_string())) + .await + .unwrap(); } } diff --git a/credsd/src/dbus/flow_control.rs b/credsd/src/dbus/flow_control.rs index db88349f..9c82b253 100644 --- a/credsd/src/dbus/flow_control.rs +++ b/credsd/src/dbus/flow_control.rs @@ -52,6 +52,7 @@ pub async fn start_flow_control_service< signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), svc, usb_pin_tx: Arc::new(AsyncMutex::new(None)), + usb_cred_tx: Arc::new(AsyncMutex::new(None)), usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), hybrid_event_forwarder_task: Arc::new(AsyncMutex::new(None)), }, @@ -72,6 +73,7 @@ struct FlowControlService { signal_state: Arc>, svc: Arc>>, usb_pin_tx: Arc>>>, + usb_cred_tx: Arc>>>, usb_event_forwarder_task: Arc>>, hybrid_event_forwarder_task: Arc>>, } @@ -181,6 +183,7 @@ where ) -> fdo::Result<()> { let mut stream = self.svc.lock().await.get_usb_credential(); let usb_pin_tx = self.usb_pin_tx.clone(); + let usb_cred_tx = self.usb_cred_tx.clone(); let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); let task = tokio::spawn(async move { @@ -214,6 +217,10 @@ where let mut usb_pin_tx = usb_pin_tx.lock().await; let _ = usb_pin_tx.insert(pin_tx); } + UsbState::SelectCredential { cred_tx, .. } => { + let mut usb_cred_tx = usb_cred_tx.lock().await; + let _ = usb_cred_tx.insert(cred_tx); + } UsbState::Completed | UsbState::Failed(_) => { break; } @@ -240,7 +247,10 @@ where } async fn select_credential(&self, credential_id: String) -> fdo::Result<()> { - todo!() + if let Some(cred_tx) = self.usb_cred_tx.lock().await.take() { + cred_tx.send(credential_id).await.unwrap(); + } + Ok(()) } #[zbus(signal)] From 2beae549cb5b0b32f29cd20ea549e3262284922d Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 20:31:56 -0500 Subject: [PATCH 34/38] Complete request over D-Bus --- creds-lib/src/server.rs | 11 ++- credsd/src/credential_service/mod.rs | 123 ++++++++++++++------------- credsd/src/dbus/flow_control.rs | 20 ++--- 3 files changed, 84 insertions(+), 70 deletions(-) diff --git a/creds-lib/src/server.rs b/creds-lib/src/server.rs index 643c9f34..aa915b7a 100644 --- a/creds-lib/src/server.rs +++ b/creds-lib/src/server.rs @@ -526,7 +526,16 @@ impl TryFrom for crate::model::UsbState { .map(|creds| Self::SelectCredential { creds }), UsbState::Completed(_) => Ok(Self::Completed), UsbState::Failed(value) => { - ServiceError::try_from(Value::<'_>::from(value)).map(|err| Self::Failed(err.into())) + let error_code: String = Value::<'_>::from(value).try_into()?; + Ok(Self::Failed( + match error_code.as_ref() { + "AuthenticatorError" => ServiceError::AuthenticatorError, + "NoCredentials" => ServiceError::NoCredentials, + "PinAttemptsExhausted" => ServiceError::PinAttemptsExhausted, + _ => ServiceError::Internal, + } + .into(), + )) } }?; Ok(ret) diff --git a/credsd/src/credential_service/mod.rs b/credsd/src/credential_service/mod.rs index baabb9cc..fbb3cc9b 100644 --- a/credsd/src/credential_service/mod.rs +++ b/credsd/src/credential_service/mod.rs @@ -14,18 +14,14 @@ use libwebauthn::{ self, ops::webauthn::{GetAssertionResponse, MakeCredentialResponse}, }; -use tokio::sync::{ - mpsc::{self, Receiver, Sender}, - Mutex as AsyncMutex, -}; +use tokio::sync::oneshot::Sender; use creds_lib::{ - client::CredentialServiceClient, model::{ CredentialRequest, CredentialResponse, Device, Error as CredentialServiceError, Operation, Transport, }, - server::{CreatePublicKeyCredentialRequest, ViewRequest}, + server::ViewRequest, }; use crate::credential_service::{hybrid::HybridEvent, usb::UsbEvent}; @@ -33,22 +29,21 @@ use crate::credential_service::{hybrid::HybridEvent, usb::UsbEvent}; use hybrid::{HybridHandler, HybridState, HybridStateInternal}; use usb::{UsbHandler, UsbStateInternal}; pub use { - server::{CredentialManagementClient, InProcessServer, UiController}, + server::{CredentialManagementClient, UiController}, usb::UsbState, }; +type RequestContext = ( + CredentialRequest, + Sender>, +); + #[derive(Debug)] pub struct CredentialService { devices: Vec, - cred_request: Mutex< - Option<( - CredentialRequest, - Sender>, - )>, - >, - // Place to store data to be returned to the caller - cred_response: Arc>>, + /// Current request and channel to respond to caller. + ctx: Arc>>, hybrid_handler: H, usb_handler: U, @@ -73,8 +68,7 @@ impl Self { devices, - cred_request: Mutex::new(None), - cred_response: Arc::new(Mutex::new(None)), + ctx: Arc::new(Mutex::new(None)), hybrid_handler, usb_handler, @@ -88,25 +82,18 @@ impl request: &CredentialRequest, tx: Sender>, ) { - // let (tx, rx) = mpsc::channel(1); - let res = { - let mut cred_request = self.cred_request.lock().unwrap(); + { + let mut cred_request = self.ctx.lock().unwrap(); if cred_request.is_some() { - drop(cred_request); - false + tx.send(Err(CredentialServiceError::Internal( + "Already a request in progress.".to_string(), + ))) + .expect("Send to local receiver to succeed"); + return; } else { - _ = cred_request.insert((request.clone(), tx.clone())); - true + _ = cred_request.insert((request.clone(), tx)); } }; - if !res { - tx.send(Err(CredentialServiceError::Internal( - "Already a request in progress.".to_string(), - ))) - .await - .expect("Send to local receiver to succeed"); - return; - } let operation = match &request { CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, @@ -120,9 +107,10 @@ impl .map_err(|err| err.to_string()); if let Err(err) = launch_ui_response { tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); - _ = self.cred_request.lock().unwrap().take(); + _ = self.ctx.lock().unwrap().take(); let err = Err(CredentialServiceError::Internal(err)); - tx.send(err).await; + let (_, tx) = self.ctx.lock().unwrap().take().unwrap(); + tx.send(err); } } @@ -133,32 +121,39 @@ impl pub fn get_hybrid_credential( &self, ) -> Pin + Send + 'static>> { - let guard = self.cred_request.lock().unwrap(); - let cred_request = guard.clone().unwrap(); - let stream = self.hybrid_handler.start(&cred_request.0); - let cred_response = self.cred_response.clone(); - Box::pin(HybridStateStream { - inner: stream, - cred_response, - }) + let guard = self.ctx.lock().unwrap(); + if let Some((ref cred_request, _)) = *guard { + let stream = self.hybrid_handler.start(&cred_request); + let ctx = self.ctx.clone(); + Box::pin(HybridStateStream { inner: stream, ctx }) + } else { + tracing::error!( + "Attempted to start hybrid credential flow, but no request context was found." + ); + todo!("Handle error when context is not set up.") + } } pub fn get_usb_credential(&self) -> Pin + Send + 'static>> { - let guard = self.cred_request.lock().unwrap(); - let cred_request = guard.clone().unwrap(); - let stream = self.usb_handler.start(&cred_request.0); - Box::pin(UsbStateStream { - inner: stream, - cred_response: self.cred_response.clone(), - }) + let guard = self.ctx.lock().unwrap(); + if let Some((ref cred_request, _)) = *guard { + let stream = self.usb_handler.start(&cred_request); + let ctx = self.ctx.clone(); + Box::pin(UsbStateStream { inner: stream, ctx }) + } else { + tracing::error!( + "Attempted to start hybrid credential flow, but no request context was found." + ); + todo!("Handle error when context is not set up.") + } } pub async fn complete_auth( &self, response: Result, ) -> () { - if let Some((_request, responder)) = self.cred_request.lock().unwrap().take() { - if responder.send(response).await.is_err() { + if let Some((_request, responder)) = self.ctx.lock().unwrap().take() { + if responder.send(response).is_err() { tracing::error!("Failed to send response to back to caller"); }; } else { @@ -169,7 +164,7 @@ impl pub struct HybridStateStream { inner: H, - cred_response: Arc>>, + ctx: Arc>>, } impl Stream for HybridStateStream @@ -182,7 +177,7 @@ where self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - let cred_response = &self.cred_response.clone(); + let ctx = &self.ctx.clone(); match Box::pin(Box::pin(self).as_mut().inner.next()).poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(Some(HybridEvent { state })) => { @@ -206,8 +201,7 @@ where ) } }; - let mut cred_response = cred_response.lock().unwrap(); - cred_response.replace(response); + complete_request(ctx, response.clone()); } Poll::Ready(Some(state.into())) } @@ -218,7 +212,7 @@ where struct UsbStateStream { inner: H, - cred_response: Arc>>, + ctx: Arc>>, } impl Stream for UsbStateStream @@ -231,13 +225,12 @@ where self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - let cred_response = &self.cred_response.clone(); + let ctx = &self.ctx.clone(); match Box::pin(Box::pin(self).as_mut().inner.next()).poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(Some(UsbEvent { state })) => { if let UsbStateInternal::Completed(response) = &state { - let mut cred_response = cred_response.lock().unwrap(); - cred_response.replace(response.clone()); + complete_request(ctx, response.clone()); } Poll::Ready(Some(state.into())) } @@ -246,6 +239,18 @@ 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." + ); + } + } else { + tracing::error!("Tried to consume context to respond to caller, but none was found.") + } +} + #[derive(Debug, Clone)] enum AuthenticatorResponse { CredentialCreated(MakeCredentialResponse), diff --git a/credsd/src/dbus/flow_control.rs b/credsd/src/dbus/flow_control.rs index 9c82b253..28708b7d 100644 --- a/credsd/src/dbus/flow_control.rs +++ b/credsd/src/dbus/flow_control.rs @@ -6,6 +6,7 @@ use creds_lib::model::{ }; use creds_lib::server::{BackgroundEvent, Device}; use futures_lite::{Stream, StreamExt}; +use tokio::sync::oneshot; use tokio::{ sync::{ mpsc::{self, Receiver, Sender}, @@ -39,7 +40,7 @@ pub async fn start_flow_control_service< Connection, Sender<( CredentialRequest, - Sender>, + oneshot::Sender>, )>, )> { let svc = Arc::new(AsyncMutex::new(credential_service)); @@ -372,7 +373,7 @@ pub trait CredentialRequestController { pub struct CredentialRequestControllerClient { pub initiator: Sender<( CredentialRequest, - Sender>, + oneshot::Sender>, )>, } @@ -381,15 +382,14 @@ impl CredentialRequestController for CredentialRequestControllerClient { &self, request: CredentialRequest, ) -> Result { - let (tx, mut rx) = mpsc::channel(4); + let (tx, rx) = oneshot::channel(); // TODO: We need a PlatformError variant. self.initiator.send((request, tx)).await.unwrap(); - if let Some(msg) = rx.recv().await { - // TODO: Pass real WebAuthnError from credential service - msg.map_err(|_| WebAuthnError::NotAllowedError) - } else { - // if the sender was dropped, then the operation is cancelled. - Err(WebAuthnError::NotAllowedError) - } + rx.await + .map_err(|_| { + tracing::error!("Credential response channel closed prematurely"); + WebAuthnError::NotAllowedError + }) + .and_then(|msg| msg.map_err(|_| WebAuthnError::NotAllowedError)) } } From bc1110080ac0b58cf28b93897f7b1fce9fbc4e54 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 20:31:56 -0500 Subject: [PATCH 35/38] Update D-Bus names in web extension --- webext/app/credential_manager_shim.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/webext/app/credential_manager_shim.py b/webext/app/credential_manager_shim.py index 109cc552..c20b6235 100755 --- a/webext/app/credential_manager_shim.py +++ b/webext/app/credential_manager_shim.py @@ -337,12 +337,13 @@ async def run(cmd, options, origin, top_origin): with open('../../contrib/xyz.iinuwa.credentials.CredentialManager.xml', 'r') as f: introspection = f.read() - proxy_object = bus.get_proxy_object('xyz.iinuwa.credentials.CredentialManagerUi', - '/xyz/iinuwa/credentials/CredentialManagerUi', - introspection) + proxy_object = bus.get_proxy_object( + "xyz.iinuwa.credentials.Credentials", + "/xyz/iinuwa/credentials/Credentials", + introspection, + ) - interface = proxy_object.get_interface( - 'xyz.iinuwa.credentials.CredentialManagerUi1') + interface = proxy_object.get_interface("xyz.iinuwa.credentials.Credentials1") logging.debug(f"Connected to interface at {interface.path}") if cmd == 'create': From d490fddf7ab56be645486619559e55ca0b799d71 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 21:58:14 -0500 Subject: [PATCH 36/38] Update webext docs --- creds-ui/data/meson.build | 2 +- webext/README.md | 51 ++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/creds-ui/data/meson.build b/creds-ui/data/meson.build index f7c54e01..8960582c 100644 --- a/creds-ui/data/meson.build +++ b/creds-ui/data/meson.build @@ -81,4 +81,4 @@ if get_option('profile') == 'development' build_by_default: true, build_always_stale: true, ) -endif +endif \ No newline at end of file diff --git a/webext/README.md b/webext/README.md index 2c9e57d1..a705239f 100644 --- a/webext/README.md +++ b/webext/README.md @@ -5,10 +5,49 @@ Currently, this is written only for Firefox; there will be some slight API tweak This requires some setup to make it work: -1. Copy `app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/credential_manager_shim.json`. -2. In the copied file, replace the `path` key with the absolute path to `app/credential_manager_shim.py` +# Prerequisites + +Currently, this web extension relies on the `dbus-next` to interact with D-Bus +services. If you have that package installed in your system Python, this +should work. You can test using the following: + +```shell +python3 -c 'import dbus_next; print("dbus-next is installed")' +``` + +If that completes without error, then you're good to go. Otherwise, you have a +couple of options: + +- Install the system package for your operating system, for example: + ```shell + # Fedora + dnf install python3-dbus-next + # Debian/Ubuntu + apt install python3-dbus-next + # Arch + pacman -S python-dbus-next + ``` +- Modify the shebang to point to a Python instance that does have the package installed. + ```shell + cd webext/ + python3 -m venv env + source ./env/bin/activate + pip3 install dbus-next + echo "Change the first line in webext/app/credential_manager_shim.py to:" + echo "#!$(readlink -f ./env/bin/python3)" + # Update the shebang to point to the absolute path to webext/env/bin/python3 + ``` + +# Setup Instructions + +(Note: Paths are relative to root of this repository) + +1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/credential_manager_shim.json`. +2. In the copied file, replace the `path` key with the absolute path to `webext/app/credential_manager_shim.py` 3. Open Firefox and go to `about:debugging` -4. Click "This Firefox" > Load Temporary Extension. Select `add-on/manifest.json` -6. Build and run the `xyz-iinuwa-credential-manager-portal-gtk` binary to start the D-Bus service. -5. Navigate to [https://webauthn.io](). -6. Run through the registration and creation process. +4. Click "This Firefox" > Load Temporary Extension. Select `webext/add-on/manifest.json` +5. Build with `ninja -C ./build` and run the following binaries binary to start the D-Bus services. + - `GSCHEMA_SCHEMA_DIR=build/creds-ui/data ./build/creds-ui/target/debug/creds-ui` + - `./build/credsd/target/debug/credsd` +6. Navigate to [https://webauthn.io](). +7. Run through the registration and creation process. From 3babaa6edf1800c9bf7a1d59d2a5febf094ec465 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 22:22:56 -0500 Subject: [PATCH 37/38] Fix D-Bus tests --- credsd/tests/config/mod.rs.in | 6 +++--- credsd/tests/dbus.rs | 2 +- credsd/tests/meson.build | 8 ++++---- .../services/xyz.iinuwa.CredentialManagerUi.service.in | 3 --- .../xyz.iinuwa.credentials.Credentials.service.in | 3 +++ 5 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 credsd/tests/services/xyz.iinuwa.CredentialManagerUi.service.in create mode 100644 credsd/tests/services/xyz.iinuwa.credentials.Credentials.service.in diff --git a/credsd/tests/config/mod.rs.in b/credsd/tests/config/mod.rs.in index b4e7d961..aab84060 100644 --- a/credsd/tests/config/mod.rs.in +++ b/credsd/tests/config/mod.rs.in @@ -1,4 +1,4 @@ pub const SERVICE_DIR: &'static str = @SERVICE_DIR@; -pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.CredentialManagerUi"; -pub const PATH: &'static str = "/xyz/iinuwa/credentials/CredentialManagerUi"; -pub const INTERFACE: &'static str = "xyz.iinuwa.credentials.CredentialManagerUi1"; +pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.Credentials"; +pub const PATH: &'static str = "/xyz/iinuwa/credentials/Credentials"; +pub const INTERFACE: &'static str = "xyz.iinuwa.credentials.Credentials1"; diff --git a/credsd/tests/dbus.rs b/credsd/tests/dbus.rs index eab5bb63..7dfc63e9 100644 --- a/credsd/tests/dbus.rs +++ b/credsd/tests/dbus.rs @@ -20,7 +20,7 @@ fn test_client_capabilities() { let capabilities = HashMap::from([ ("conditionalCreate", false), ("conditionalGet", false), - ("hybridTransport", false), + ("hybridTransport", true), ("passkeyPlatformAuthenticator", false), ("userVerifyingPlatformAuthenticator", false), ("relatedOrigins", false), diff --git a/credsd/tests/meson.build b/credsd/tests/meson.build index 390066e0..48aeafda 100644 --- a/credsd/tests/meson.build +++ b/credsd/tests/meson.build @@ -5,7 +5,7 @@ test_config.set_quoted( ) test_config.set( 'DBUS_EXECUTABLE', - meson.project_build_root() / backend_build_dir / backend_executable_name, + meson.project_build_root() / backend_build_dir / 'target' / rust_target / backend_executable_name, ) configure_file( input: 'config' / 'mod.rs.in', @@ -22,8 +22,8 @@ run_command( ) configure_file( - input: 'services' / 'xyz.iinuwa.CredentialManagerUi.service.in', - output: 'xyz.iinuwa.CredentialManagerUi.service', + input: 'services' / 'xyz.iinuwa.credentials.Credentials.service.in', + output: 'xyz.iinuwa.credentials.Credentials.service', configuration: test_config, ) @@ -40,4 +40,4 @@ test( ], protocol: 'exitcode', verbose: true, -) +) \ No newline at end of file diff --git a/credsd/tests/services/xyz.iinuwa.CredentialManagerUi.service.in b/credsd/tests/services/xyz.iinuwa.CredentialManagerUi.service.in deleted file mode 100644 index 9fc84ead..00000000 --- a/credsd/tests/services/xyz.iinuwa.CredentialManagerUi.service.in +++ /dev/null @@ -1,3 +0,0 @@ -[D-BUS Service] -Name=xyz.iinuwa.credentials.CredentialManagerUi -Exec=@DBUS_EXECUTABLE@ diff --git a/credsd/tests/services/xyz.iinuwa.credentials.Credentials.service.in b/credsd/tests/services/xyz.iinuwa.credentials.Credentials.service.in new file mode 100644 index 00000000..a32a18aa --- /dev/null +++ b/credsd/tests/services/xyz.iinuwa.credentials.Credentials.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=xyz.iinuwa.credentials.Credentials +Exec=@DBUS_EXECUTABLE@ From d591301772635c8609a814525a594b0aa2e67c22 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 4 Aug 2025 22:29:26 -0500 Subject: [PATCH 38/38] Update references to xyz-iinuwa-credential-manager-portal-gtk --- .github/workflows/main.yml | 4 +- README.md | 54 ++++++++++--------- ...z.iinuwa.CredentialManagerUi.desktop.in.in | 2 +- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b760e269..645244fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,7 @@ jobs: run: meson test --interactive working-directory: build/ - name: Check clippy recommendations - run: env CARGO_HOME=build/cargo-home cargo clippy --manifest-path xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml --target-dir build/xyz-iinuwa-credential-manager-portal-gtk/src + run: env CARGO_HOME=build/cargo-home cargo clippy --manifest-path credsd/Cargo.toml --target-dir build/credsd/target/release - name: Check formatting run: cargo fmt --check - working-directory: xyz-iinuwa-credential-manager-portal-gtk + working-directory: credsd diff --git a/README.md b/README.md index 2dd6e8a0..862b433e 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,16 @@ This project uses Meson and Ninja. Package requirements: - - GTK4 - - gettext - - libdbus-1 - - libssl/openssl - - libudev - - desktop-file-utils + +- GTK4 +- gettext +- libdbus-1 +- libssl/openssl +- libudev +- desktop-file-utils For example, on Ubuntu: + ```shell sudo apt update && sudo apt install \ # Build dependencies @@ -42,9 +44,10 @@ ninja -C build ```shell # Run the server, with debug logging enabled -export GSETTINGS_SCHEMA_DIR=build/xyz-iinuwa-credential-manager-portal-gtk/data -export RUST_LOG=xyz_iinuwa_credential_manager_portal_gtk=debug -./build/xyz-iinuwa-credential-manager-portal-gtk/src/xyz-iinuwa-credential-manager-portal-gtk +export GSETTINGS_SCHEMA_DIR=build/creds-ui/data +export RUST_LOG=credsd=debug,creds_ui=debug +./build/credsd/target/debug/credsd & +./build/creds-ui/target/debug/creds-ui ``` ### Clients @@ -59,7 +62,6 @@ cd demo_client/ There is also a demo web extension that can be used to test the service in Firefox. Instructions are in [webext/README.md](). - ## Goals The goal of this repository is to define a spec for clients (apps, browsers, @@ -77,29 +79,30 @@ Some high-level goals: etc.) to hook into Some nice-to-haves: + - Design a specification for a platform authenticator. I'm not sure whether this -needs to be specified, or whether it could be considered and implemented as a -first-party credential provider. + needs to be specified, or whether it could be considered and implemented as a + first-party credential provider. Some non-goals: - Fully integrate with any specific desktop environment. Each desktop -environment (GNOME, KDE, etc.) has its own UI and UX conventions, as well as -system configuration methods (e.g., GNOME Settings), which this API will need to integrate with. -Because of the variation, we intend to leave integration with these other -components to developers more familiar with each of the desktop environments. -For now, we are using bare GTK to build a UI for testing, but any UI -implementation in this repository is for reference purposes. If anyone is willing to do some of this integration work, feel free to contact us! + environment (GNOME, KDE, etc.) has its own UI and UX conventions, as well as + system configuration methods (e.g., GNOME Settings), which this API will need to integrate with. + Because of the variation, we intend to leave integration with these other + components to developers more familiar with each of the desktop environments. + For now, we are using bare GTK to build a UI for testing, but any UI + implementation in this repository is for reference purposes. If anyone is willing to do some of this integration work, feel free to contact us! - Create a full-featured password manager. Features like Password syncing, -password generation, rotation, etc. is not part of this specficiation. Other -password manager projects should be able to use this to make their credentials -available to the user uniformly, though. + password generation, rotation, etc. is not part of this specficiation. Other + password manager projects should be able to use this to make their credentials + available to the user uniformly, though. - BSD support. While we'd love to help out all open desktop environments, we don't -know enough about any BSD to make it useful for them. Hopefully, the design -process is transparent enough that someone else could design something that -works for BSDs. + know enough about any BSD to make it useful for them. Hopefully, the design + process is transparent enough that someone else could design something that + works for BSDs. ## Current Work @@ -142,9 +145,8 @@ Alternatively, lock out the credential based on incorrect attempts. ![](images/security-key-3.png) ![](images/end.png) - - ## Related projects: + - https://github.com/linux-credentials/libwebauthn (previously https://github.com/AlfioEmanueleFresta/xdg-credentials-portal) - authenticator-rs - webauthn-rs diff --git a/creds-ui/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in b/creds-ui/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in index 4bf41a81..10694f2e 100644 --- a/creds-ui/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in +++ b/creds-ui/data/xyz.iinuwa.CredentialManagerUi.desktop.in.in @@ -2,7 +2,7 @@ Name=Credential Manager Comment=Write a GTK + Rust application Type=Application -Exec=xyz-iinuwa-credential-manager-portal-gtk +Exec=credsd Terminal=false Categories=GNOME;GTK; # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!