From dd5d1e09c79e469f7d0e1a857eff4419b6c70da5 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 16 May 2026 08:45:43 -0500 Subject: [PATCH] daemon,ui: Send PIN over fd --- Cargo.lock | 7 ++- Cargo.toml | 3 +- credentialsd-common/src/model.rs | 7 ++- credentialsd-common/src/server.rs | 19 ++---- credentialsd-ui/Cargo.toml | 2 + credentialsd-ui/src/client.rs | 83 ++++++++++++++++++++++++++- credentialsd/Cargo.toml | 1 + credentialsd/src/dbus/flow_control.rs | 75 +++++++++++++++++++++++- 8 files changed, 174 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6d9f7f8..a1c226f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -756,6 +756,7 @@ dependencies = [ "credentialsd-common", "futures-lite", "gio", + "libc", "libwebauthn", "nfc1", "rand 0.9.2", @@ -787,11 +788,13 @@ dependencies = [ "gdk4-wayland", "gettext-rs", "gtk4", + "libc", "qrcode", "serde", "tracing", "tracing-subscriber", "zbus", + "zeroize", ] [[package]] @@ -2044,9 +2047,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" diff --git a/Cargo.toml b/Cargo.toml index f5020613..e9bd3a7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,9 @@ lto = true [workspace.dependencies] futures-lite = "2.6.0" +libc = "0.2.186" serde = { version = "1.0.219", features = ["derive"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" -zvariant = "5.6.0" zbus = { version = "5.9.0", default-features = false } +zvariant = "5.6.0" diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 31ad1543..1ac9e8c3 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -1,7 +1,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use zvariant::{Optional, SerializeDict, Type}; +use zvariant::{Optional, OwnedFd, SerializeDict, Type}; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Credential { @@ -279,8 +279,9 @@ pub enum UserInteractedEvent { /// Start USB discovery UsbDiscoveryRequested, - /// Send client PIN - ClientPinEntered(String), + /// Send client PIN. Length of the PIN MUST not be greater than 63 bytes. + /// File descriptor must be memory-mapped to be read. + ClientPinEntered(OwnedFd), /// Select a credential by credential ID CredentialSelected(String), diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index 5d25925b..6ab4fd81 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -7,7 +7,7 @@ use serde::{ de::{DeserializeSeed, Error, Visitor}, }; use zvariant::{ - self, Array, DeserializeDict, DynamicDeserialize, NoneValue, Optional, OwnedValue, + self, Array, DeserializeDict, DynamicDeserialize, Fd, NoneValue, Optional, OwnedFd, OwnedValue, SerializeDict, Signature, Str, Structure, StructureBuilder, Type, Value, signature::Fields, }; @@ -435,9 +435,9 @@ impl From<&UserInteractedEvent> for Structure<'_> { UserInteractedEvent::UsbDiscoveryRequested => { tag_value_to_struct(USER_INTERACTED_EVENT_USB_DISCOVERY_REQUESTED, None) } - UserInteractedEvent::ClientPinEntered(pin) => tag_value_to_struct( + UserInteractedEvent::ClientPinEntered(pin_fd) => tag_value_to_struct( USER_INTERACTED_EVENT_CLIENT_PIN_ENTERED, - Some(Value::Str(pin.into())), + Some(Value::Fd(pin_fd.into())), ), UserInteractedEvent::CredentialSelected(credential_id) => tag_value_to_struct( USER_INTERACTED_EVENT_CREDENTIAL_SELECTED, @@ -467,16 +467,9 @@ impl TryFrom<&Structure<'_>> for UserInteractedEvent { Ok(UserInteractedEvent::UsbDiscoveryRequested) } USER_INTERACTED_EVENT_CLIENT_PIN_ENTERED => { - let s: Str = value.downcast_ref()?; - if s.is_empty() { - return Err(zvariant::Error::invalid_length( - s.len(), - &"a non-empty string", - )); - } - Ok(UserInteractedEvent::ClientPinEntered( - s.as_str().to_string(), - )) + let fd = value.downcast_ref::()?; + let owned_fd = fd.try_to_owned()?; + Ok(UserInteractedEvent::ClientPinEntered(owned_fd.into())) } USER_INTERACTED_EVENT_CREDENTIAL_SELECTED => { let s: Str = value.downcast_ref()?; diff --git a/credentialsd-ui/Cargo.toml b/credentialsd-ui/Cargo.toml index 7397fc10..56f126b4 100644 --- a/credentialsd-ui/Cargo.toml +++ b/credentialsd-ui/Cargo.toml @@ -16,8 +16,10 @@ futures-lite = "2.6.0" gettext-rs = { version = "0.7", features = ["gettext-system"] } gtk = { version = "0.10.3", package = "gtk4", features = ["v4_6"] } gdk-wayland = { version = "0.10.3", package = "gdk4-wayland", optional = true } +libc.workspace = true qrcode = "0.14.1" serde.workspace = true tracing.workspace = true tracing-subscriber = "0.3.19" +zeroize = { version = "1.8.2" } zbus = { workspace = true, default-features = false, features = ["async-io"]} diff --git a/credentialsd-ui/src/client.rs b/credentialsd-ui/src/client.rs index b799a2f2..2366184f 100644 --- a/credentialsd-ui/src/client.rs +++ b/credentialsd-ui/src/client.rs @@ -1,7 +1,20 @@ +use std::{ + io::ErrorKind, + os::{ + fd::{FromRawFd, OwnedFd}, + raw::c_void, + }, +}; + use async_std::{ channel::{Receiver, Sender}, sync::Mutex as AsyncMutex, }; +use libc::{ + MAP_SHARED, O_CLOEXEC, PROT_READ, PROT_WRITE, SYS_memfd_secret, ftruncate, mmap, off_t, +}; +use zeroize::Zeroize; + use credentialsd_common::{model::UserInteractedEvent, server::BackgroundEvent}; #[derive(Debug)] @@ -25,7 +38,17 @@ impl FlowControlClient { } pub async fn enter_client_pin(&mut self, pin: String) -> Result<(), ()> { - self.send(UserInteractedEvent::ClientPinEntered(pin)).await + let fd = match write_secret(pin) { + Ok(fd) => fd, + Err(err) => { + tracing::error!(%err, "Failed to write secret to file descriptor"); + // TODO: need to send a message back to GUI thread that there was an error. + _ = self.cancel_request().await; + return Err(()); + } + }; + self.send(UserInteractedEvent::ClientPinEntered(fd.into())) + .await } pub async fn select_credential(&self, credential_id: String) -> Result<(), ()> { @@ -52,3 +75,61 @@ impl FlowControlClient { } } } + +fn write_secret(secret: String) -> Result { + let mut bytes = secret.into_bytes(); + // CTAP pins are maximum of 63 bytes, so they should all fit in a u8. + let bytes_len = if bytes.len() <= 63 { + bytes.len() as u8 + } else { + return Err(std::io::Error::new( + ErrorKind::FileTooLarge, + "value is too large", + )); + }; + + // Open memfd_secret + let ret: i64 = unsafe { libc::syscall(SYS_memfd_secret, O_CLOEXEC) }; + if ret == -1 { + tracing::debug!("Failed to create memfd_secret"); + return Err(std::io::Error::last_os_error()); + } + let fd = i32::try_from(ret).map_err(|_| std::io::Error::other("invalid file descriptor"))?; + if unsafe { ftruncate(fd, bytes_len as off_t) } == -1 { + tracing::debug!("Failed to ftruncate memfd_secret"); + return Err(std::io::Error::last_os_error()); + } + + let ptr = unsafe { + let ptr = mmap( + std::ptr::null_mut(), + 4096, + PROT_READ | PROT_WRITE, + MAP_SHARED, + fd, + 0, + ); + if ptr == usize::MAX as *mut c_void { + tracing::debug!("Failed to mmap memfd_secret"); + return Err(std::io::Error::last_os_error()); + } + // ptr as *mut u8 + ptr + }; + + // Copy the data + unsafe { + ptr.copy_from_nonoverlapping(bytes.as_ptr().cast(), bytes.len()); + } + + // Cleanup + if unsafe { libc::munmap(ptr, 4096) } == -1 { + tracing::debug!("Failed to unmap memfd_secret"); + return Err(std::io::Error::last_os_error()); + } + bytes.zeroize(); + drop(bytes); + + let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) }; + Ok(owned_fd) +} diff --git a/credentialsd/Cargo.toml b/credentialsd/Cargo.toml index dcb5302d..10e0115c 100644 --- a/credentialsd/Cargo.toml +++ b/credentialsd/Cargo.toml @@ -11,6 +11,7 @@ async-trait = "0.1.89" base64 = "0.22.1" credentialsd-common = { path = "../credentialsd-common" } futures-lite.workspace = true +libc.workspace = true libwebauthn = { version = "0.3.0", features = ["libnfc","pcsc"] } # TODO: split nfc and pcsc into separate features # Also, 0.6.1 fails to build with non-vendored library. diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index 7761aaac..8e8982b9 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -1,8 +1,13 @@ //! This module implements the service to allow the user to control the flow of //! the credential request through the trusted UI. -use std::sync::Mutex; -use std::{fmt::Debug, sync::Arc}; +use std::{ + fmt::Debug, + io::{self, ErrorKind}, + mem::MaybeUninit, + os::{fd::AsRawFd, raw::c_void}, + sync::{Arc, Mutex}, +}; use async_trait::async_trait; use credentialsd_common::model::{ @@ -11,6 +16,7 @@ use credentialsd_common::model::{ }; use credentialsd_common::server::{BackgroundEvent, WindowHandle}; use futures_lite::{Stream, StreamExt}; +use libc::{MAP_SHARED, PROT_READ, PROT_WRITE}; use tokio::sync::mpsc::Receiver; use tokio::sync::oneshot; use tokio::sync::{mpsc::Sender, Mutex as AsyncMutex}; @@ -191,7 +197,16 @@ async fn handle { + UserInteractedEvent::ClientPinEntered(pin_fd) => { + let pin_fd = std::os::fd::OwnedFd::from(pin_fd); + let pin = match read_secret(pin_fd.into()) { + Ok(pin) => pin, + // TODO: need to send an error to the UI, cancel the request and terminate the loop. + Err(err) => { + tracing::error!(%err, "Failed to read client PIN. Stopping event loop. TODO: cancel the request"); + break; + } + }; let tx = { client_pin_tx.lock().unwrap().take() }; if let Some(tx) = tx { if tx.send(pin).await.is_err() { @@ -299,6 +314,60 @@ impl CredentialRequestController for CredentialRequestControllerClient { } } +fn read_secret(pin_fd: std::os::fd::OwnedFd) -> Result { + // Get pin length + let len = { + let mut stat_buf = MaybeUninit::::uninit(); + let res = unsafe { libc::fstat(pin_fd.as_raw_fd(), stat_buf.as_mut_ptr()) }; + if res == -1 { + return Err(io::Error::last_os_error()); + } + let stat_buf = unsafe { stat_buf.assume_init() }; + usize::try_from(stat_buf.st_size) + .map_err(|_| io::Error::new(ErrorKind::FileTooLarge, "pin is too large"))? + }; + + // map the memory from the file descriptor + let ptr = unsafe { + let ptr = libc::mmap( + std::ptr::null_mut(), + 4096, + PROT_READ | PROT_WRITE, + MAP_SHARED, + pin_fd.as_raw_fd(), + 0, + ); + if ptr == usize::MAX as *mut c_void { + return Err(std::io::Error::last_os_error()); + } + ptr as *const u8 + }; + + // Copy the bytes. + let buf = unsafe { + // let len = ptr.read() as usize; + let mut buf: Vec = Vec::with_capacity(len); + ptr.copy_to_nonoverlapping(buf.as_mut_ptr().cast(), len); + buf.set_len(len); + buf + }; + + // Clean up mapping + unsafe { + if libc::munmap(ptr as *mut c_void, 4096) == -1 { + return Err(std::io::Error::last_os_error()); + } + } + drop(pin_fd); + + String::from_utf8(buf).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid UTF-8 data found in buffer", + ) + }) +} + #[cfg(test)] pub mod test { use std::{