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
11 changes: 6 additions & 5 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions credentialsd-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ license = "LGPL-3.0-only"

[dependencies]
futures-lite.workspace = true
libc.workspace = true
serde = { workspace = true, features = ["derive"] }
tracing.workspace = true
zeroize = "1.9.0"
zvariant.workspace = true
1 change: 1 addition & 0 deletions credentialsd-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod client;
pub mod memfd;
pub mod model;
pub mod server;
226 changes: 226 additions & 0 deletions credentialsd-common/src/memfd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
use std::{
io::{self, ErrorKind, Read, Write},
mem::{ManuallyDrop, MaybeUninit},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
raw::c_void,
},
ptr::{self, NonNull},
};

use libc::{
MAP_SHARED, MS_SYNC, O_CLOEXEC, PROT_READ, PROT_WRITE, SYS_memfd_secret, fstat, ftruncate,
mmap, msync, munmap, off_t, syscall,
};
use zeroize::Zeroize;

/// On most architectures, the minimum page size is 4KB, so we use that as a baseline for creating
/// memory-mapped files. If the page size is larger, the OS will just map a larger page than we
/// need.
const MIN_PAGE_SIZE: usize = 4096;

/// Read a secret from a memory-mapped file.
pub fn read_secret(fd: OwnedFd) -> io::Result<Vec<u8>> {
let mut reader = MmapReader::from_fd(fd)?;
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
Ok(buf)
}

/// Write a secret to a memfd_secret and return its file descriptor. The secret will be zeroized
/// from memory.
pub fn write_secret(mut secret: Vec<u8>) -> io::Result<OwnedFd> {
// For now, we're only accepting values that fit within a single page.
// This can be raised in the future if needed.
if secret.len() > MIN_PAGE_SIZE {
return Err(io::Error::new(
ErrorKind::FileTooLarge,
"value is too large",
));
}

let memfd_secret = open_memfd_secret(secret.len())?;
let mut mem = Mmap::from_fd(memfd_secret)?;
mem.write_all(&secret)?;
secret.zeroize();
drop(secret);

Ok(mem.into_fd())
}

struct Mmap {
inner: NonNull<u8>,
fd: OwnedFd,
size: usize,
pos: usize,
}

impl Mmap {
fn from_fd(fd: OwnedFd) -> io::Result<Self> {
let size = MIN_PAGE_SIZE;
let ptr = unsafe {
let ptr = mmap(
ptr::null_mut(),
size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd.as_raw_fd(),
0,
);
if ptr == usize::MAX as *mut c_void {
let err = io::Error::last_os_error();
tracing::error!(%err, "mmap failed");
return Err(err);
}
NonNull::new(ptr as *mut u8)
.ok_or_else(|| io::Error::other("mmap returned NULL pointer"))?
};

return Ok(Self {
inner: ptr,
fd,
size,
pos: 0,
});
}

fn into_fd(self) -> OwnedFd {
let mmap = ManuallyDrop::new(self);
assert!(unsafe { munmap(mmap.inner.as_ptr().cast(), 4096) != -1 });
unsafe { ptr::read(&mmap.fd) }
}
}

impl Write for Mmap {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let remaining = self.size - self.pos;
if remaining == 0 {
return Ok(0);
}

let bytes_to_write = usize::min(remaining, buf.len());
unsafe {
self.inner
.as_ptr()
.wrapping_add(self.pos)
.copy_from_nonoverlapping(buf.as_ptr(), bytes_to_write)
};
self.pos += bytes_to_write;

Ok(bytes_to_write)
}

fn flush(&mut self) -> io::Result<()> {
// No-op if there's no bytes to flush, prevents errors on call to msyc
if self.pos == 0 {
return Ok(());
}

if unsafe { msync(self.inner.as_ptr().cast(), self.pos, MS_SYNC) == -1 } {
let err = io::Error::last_os_error();
// msync is invalid on some file descriptors, so we ignore the error if called on one of those.
let ErrorKind::InvalidInput = err.kind() else {
tracing::error!("Failed to flush bytes");
return Err(err);
};
}
Ok(())
}
}

impl Drop for Mmap {
fn drop(&mut self) {
unsafe {
assert!(munmap(self.inner.as_ptr().cast(), MIN_PAGE_SIZE) != -1);
}
}
}

struct MmapReader {
inner: NonNull<u8>,
_fd: OwnedFd,
size: usize,
pos: usize,
}

impl MmapReader {
fn from_fd(fd: OwnedFd) -> io::Result<Self> {
let ptr = unsafe {
let size = MIN_PAGE_SIZE;
let flags = PROT_READ;
let ptr = mmap(ptr::null_mut(), size, flags, MAP_SHARED, fd.as_raw_fd(), 0);
if ptr == usize::MAX as *mut c_void {
let err = io::Error::last_os_error();
tracing::error!(%err, "mmap failed");
return Err(err);
}
NonNull::new(ptr as *mut u8)
.ok_or_else(|| io::Error::other("mmap returned NULL pointer"))?
};

// actual size of the data in the file
let size = {
let mut stat_buf = MaybeUninit::<libc::stat>::uninit();
let res = unsafe { fstat(fd.as_raw_fd(), stat_buf.as_mut_ptr()) };
if res == -1 {
tracing::error!("fstat failed");
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, "file is too large"))?
};
Ok(Self {
inner: ptr,
_fd: fd,
size,
pos: 0,
})
}
}

impl Drop for MmapReader {
fn drop(&mut self) {
unsafe {
assert!(munmap(self.inner.as_ptr().cast(), MIN_PAGE_SIZE) != -1);
}
}
}

impl Read for MmapReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let remaining = self.size - self.pos;
if remaining == 0 {
return Ok(0);
}
let bytes_to_read = usize::min(remaining, buf.len());
unsafe {
self.inner
.as_ptr()
.wrapping_add(self.pos)
.copy_to_nonoverlapping(buf.as_mut_ptr(), bytes_to_read);
}
self.pos += bytes_to_read;
Ok(bytes_to_read)
}
}

fn open_memfd_secret(len: usize) -> io::Result<OwnedFd> {
let len = off_t::try_from(len)
.map_err(|_| io::Error::new(ErrorKind::FileTooLarge, "File is too large"))?;

// Open memfd_secret
let fd = {
let ret = unsafe { syscall(SYS_memfd_secret, O_CLOEXEC) };
if ret == -1 {
return Err(io::Error::last_os_error());
}
unsafe { OwnedFd::from_raw_fd(ret as i32) }
};

// Set length on fd. We have to do this before memory-mapping it.
if unsafe { ftruncate(fd.as_raw_fd(), len) } == -1 {
return Err(io::Error::last_os_error());
}
Ok(fd)
}
43 changes: 24 additions & 19 deletions credentialsd-common/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const USER_INTERACTED_EVENT_CREDENTIAL_SELECTED: u32 = 0x05;
const USER_INTERACTED_EVENT_REQUEST_CANCELLED: u32 = 0x06;

/// Flattened enum BackgroundEvent for sending across D-Bus.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, PartialEq)]
pub enum BackgroundEvent {
CeremonyCompleted,
NeedsPin { attempts_left: Option<u32> },
Expand All @@ -64,7 +64,7 @@ pub enum BackgroundEvent {
SelectingCredential { creds: Vec<Credential> },

HybridIdle,
HybridStarted(String),
HybridStarted(OwnedFd),
HybridConnecting,
HybridConnected,

Expand Down Expand Up @@ -138,7 +138,7 @@ impl From<&BackgroundEvent> for Structure<'_> {
Some(Value::U32(attempts_left.map(u32::from).unwrap_or(u32::MAX)))
}
BackgroundEvent::SelectingCredential { creds } => Some(Value::Array(creds.into())),
BackgroundEvent::HybridStarted(qr_data) => Some(Value::Str(qr_data.into())),
BackgroundEvent::HybridStarted(qr_data_fd) => Some(Value::Fd(qr_data_fd.into())),
// Empty
BackgroundEvent::CeremonyCompleted => None,
BackgroundEvent::NeedsUserPresence => None,
Expand Down Expand Up @@ -215,8 +215,8 @@ impl TryFrom<&Structure<'_>> for BackgroundEvent {

BACKGROUND_EVENT_HYBRID_IDLE => Ok(Self::HybridIdle),
BACKGROUND_EVENT_HYBRID_STARTED => {
let qr_data = value.downcast_ref::<&str>()?;
Ok(Self::HybridStarted(qr_data.to_string()))
let qr_data_fd = value.downcast_ref::<Fd>()?.try_to_owned()?;
Ok(Self::HybridStarted(qr_data_fd.into()))
}
BACKGROUND_EVENT_HYBRID_CONNECTING => Ok(Self::HybridConnecting),
BACKGROUND_EVENT_HYBRID_CONNECTED => Ok(Self::HybridConnected),
Expand Down Expand Up @@ -636,6 +636,8 @@ fn tag_value_to_struct(tag: u32, value: Option<Value<'_>>) -> Structure<'static>

#[cfg(test)]
mod test {
use std::os::fd::{FromRawFd, OwnedFd};

use zvariant::{
Type,
serialized::{Context, Data, Format},
Expand All @@ -656,25 +658,28 @@ mod test {

#[test]
fn test_round_trip_background_hybrid_event() {
let event1 = BackgroundEvent::HybridStarted("FIDO:/1234".to_string());
let mut fds = [0; 2];
unsafe {
libc::pipe(fds.as_mut_ptr());
}
// Wrap the raw fds into safe OwnedFd instances
let mock_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
let _mock_fd2 = unsafe { OwnedFd::from_raw_fd(fds[1]) };

println!("mock_fd: {mock_fd:?}");
let event1 = BackgroundEvent::HybridStarted(mock_fd.into());
let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0);
assert_eq!("(uv)", BackgroundEvent::SIGNATURE.to_string());
let data = zvariant::to_bytes(ctx, &event1).unwrap();
let expected = b"\x00\x00\x00\x21\x01s\0\0\0\0\0\x0aFIDO:/1234\0";
println!("data: {data:?}");
// handle value is 0_u32 because it's the index into the list of fds in the message. Since
// there's only one, it will be 0.
let expected = b"\x00\x00\x00\x21\x01h\0\0\0\0\0\0";
assert_eq!(expected, data.bytes());
let event2 = data.deserialize().unwrap().0;
assert_eq!(event1, event2);
}

#[test]
fn test_deserialize_background_hybrid_event() {
let bytes = b"\x00\x00\x00\x21\x01s\0\0\0\0\0\x0aFIDO:/1234\0";
let data = Data::new(bytes, Context::new(Format::DBus, zvariant::BE, 0));
let event: BackgroundEvent = data.deserialize().unwrap().0;
assert!(matches!(
event,
BackgroundEvent::HybridStarted(ref s) if s == "FIDO:/1234"
));
// I believe that the fd is `dup()`'d through the serialization/deserialization process, so
// we can't compare the numbers for equality.
assert!(matches!(event2, BackgroundEvent::HybridStarted(_)));
}

#[test]
Expand Down
1 change: 0 additions & 1 deletion credentialsd-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ futures-lite.workspace = true
gettext-rs = { version = "0.7", features = ["gettext-system"] }
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_14"] }
gdk-wayland = { version = "0.10.3", package = "gdk4-wayland", optional = true }
libc.workspace = true
qrcode = "0.14.1"
serde.workspace = true
tracing.workspace = true
Expand Down
1 change: 1 addition & 0 deletions credentialsd-ui/data/resources/ui/window.blp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ template $CredentialsUiWindow: ApplicationWindow {
visible: bind template.view-model as <$CredentialManagerViewModel>.usb_nfc_pin_entry_visible;
placeholder-text: _("Enter your device PIN");
can-focus: true;
max-width-chars: 63;
}
};
}
Expand Down
Loading
Loading