Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 4 additions & 3 deletions credentialsd-common/src/model.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
19 changes: 6 additions & 13 deletions credentialsd-common/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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::<Fd>()?;
let owned_fd = fd.try_to_owned()?;
Ok(UserInteractedEvent::ClientPinEntered(owned_fd.into()))
}
USER_INTERACTED_EVENT_CREDENTIAL_SELECTED => {
let s: Str = value.downcast_ref()?;
Expand Down
2 changes: 2 additions & 0 deletions credentialsd-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
83 changes: 82 additions & 1 deletion credentialsd-ui/src/client.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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<(), ()> {
Expand All @@ -52,3 +75,61 @@ impl FlowControlClient {
}
}
}

fn write_secret(secret: String) -> Result<OwnedFd, std::io::Error> {
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)
}
1 change: 1 addition & 0 deletions credentialsd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
75 changes: 72 additions & 3 deletions credentialsd/src/dbus/flow_control.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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};
Expand Down Expand Up @@ -191,7 +197,16 @@ async fn handle<M: ManageDevice + Debug + Send + Sync + 'static, UC: UiControlle
let flow = flow.clone();
forward_background_event_stream(flow, stream);
}
UserInteractedEvent::ClientPinEntered(pin) => {
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() {
Expand Down Expand Up @@ -299,6 +314,60 @@ impl CredentialRequestController for CredentialRequestControllerClient {
}
}

fn read_secret(pin_fd: std::os::fd::OwnedFd) -> Result<String, std::io::Error> {
// Get pin length
let len = {
let mut stat_buf = MaybeUninit::<libc::stat>::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<u8> = 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::{
Expand Down
Loading