// ABOUTME: fprintd D-Bus probe for fingerprint device availability. // ABOUTME: Checks if fprintd is running and the user has enrolled fingerprints. use gio::prelude::*; use gtk4::gio; const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint"; const FPRINTD_MANAGER_PATH: &str = "/net/reactivated/Fprint/Manager"; const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager"; const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device"; const DBUS_TIMEOUT_MS: i32 = 3000; const FPRINTD_DEVICE_PREFIX: &str = "/net/reactivated/Fprint/Device/"; /// Lightweight fprintd probe — detects device availability and finger enrollment. /// Does NOT perform verification (that happens through greetd/PAM). pub struct FingerprintProbe { device_proxy: Option, } impl FingerprintProbe { /// Create a probe without any D-Bus connections. /// Call `init_async().await` to connect to fprintd. pub fn new() -> Self { FingerprintProbe { device_proxy: None, } } /// Connect to fprintd on the system bus and discover the default device. pub async fn init_async(&mut self) { let manager = match gio::DBusProxy::for_bus_future( gio::BusType::System, gio::DBusProxyFlags::NONE, None, FPRINTD_BUS_NAME, FPRINTD_MANAGER_PATH, FPRINTD_MANAGER_IFACE, ) .await { Ok(m) => m, Err(e) => { log::debug!("fprintd manager not available: {e}"); return; } }; let result = match manager .call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) .await { Ok(r) => r, Err(e) => { log::debug!("fprintd GetDefaultDevice failed: {e}"); return; } }; let device_path = match result.child_value(0).get::() { Some(p) => p, None => { log::debug!("fprintd: unexpected GetDefaultDevice response type"); return; } }; if device_path.is_empty() { return; } if !device_path.starts_with(FPRINTD_DEVICE_PREFIX) { log::warn!("Unexpected fprintd device path: {device_path}"); return; } match gio::DBusProxy::for_bus_future( gio::BusType::System, gio::DBusProxyFlags::NONE, None, FPRINTD_BUS_NAME, &device_path, FPRINTD_DEVICE_IFACE, ) .await { Ok(proxy) => { self.device_proxy = Some(proxy); } Err(e) => { log::debug!("fprintd device proxy failed: {e}"); } } } /// Check if the user has enrolled fingerprints on the default device. /// Returns false if fprintd is unavailable or the user has no enrollments. pub async fn is_available_async(&self, username: &str) -> bool { let proxy = match &self.device_proxy { Some(p) => p, None => return false, }; let args = glib::Variant::from((&username,)); match proxy .call_future( "ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS, ) .await { Ok(result) => match result.child_value(0).get::>() { Some(fingers) => !fingers.is_empty(), None => { log::debug!("fprintd: unexpected ListEnrolledFingers response type"); false } }, Err(_) => false, } } } #[cfg(test)] mod tests { use super::*; #[test] fn new_probe_has_no_device() { let probe = FingerprintProbe::new(); assert!(probe.device_proxy.is_none()); } #[test] fn constants_are_defined() { assert!(!FPRINTD_BUS_NAME.is_empty()); assert!(!FPRINTD_MANAGER_PATH.is_empty()); assert!(!FPRINTD_MANAGER_IFACE.is_empty()); assert!(!FPRINTD_DEVICE_IFACE.is_empty()); assert!(DBUS_TIMEOUT_MS > 0); } }