Rewrite moonlock from Python to Rust (v0.4.0)
Complete rewrite of the Wayland lockscreen from Python/PyGObject to Rust/gtk4-rs for memory safety in security-critical PAM code and consistency with the moonset/moongreet Rust ecosystem. Modules: main, lockscreen, auth (PAM FFI), fingerprint (fprintd D-Bus), config, i18n, users, power. 37 unit tests. Security: PAM conversation callback with Zeroizing password, panic hook that never unlocks, root check, ext-session-lock-v1 compositor policy, absolute loginctl path, avatar symlink rejection.
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
// ABOUTME: fprintd D-Bus integration for fingerprint authentication.
|
||||
// ABOUTME: Provides FingerprintListener that connects to fprintd via Gio.DBusProxy.
|
||||
|
||||
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 MAX_FP_ATTEMPTS: u32 = 10;
|
||||
|
||||
/// Retry-able statuses — finger not read properly, try again.
|
||||
const RETRY_STATUSES: &[&str] = &[
|
||||
"verify-swipe-too-short",
|
||||
"verify-finger-not-centered",
|
||||
"verify-remove-and-retry",
|
||||
"verify-retry-scan",
|
||||
];
|
||||
|
||||
/// Fingerprint listener state.
|
||||
pub struct FingerprintListener {
|
||||
device_proxy: Option<gio::DBusProxy>,
|
||||
signal_id: Option<glib::SignalHandlerId>,
|
||||
running: bool,
|
||||
failed_attempts: u32,
|
||||
on_success: Option<Box<dyn Fn() + 'static>>,
|
||||
on_failure: Option<Box<dyn Fn() + 'static>>,
|
||||
}
|
||||
|
||||
impl FingerprintListener {
|
||||
/// Create a new FingerprintListener.
|
||||
/// Connects to fprintd synchronously — call before creating GTK windows.
|
||||
pub fn new() -> Self {
|
||||
let mut listener = FingerprintListener {
|
||||
device_proxy: None,
|
||||
signal_id: None,
|
||||
running: false,
|
||||
failed_attempts: 0,
|
||||
on_success: None,
|
||||
on_failure: None,
|
||||
};
|
||||
listener.init_device();
|
||||
listener
|
||||
}
|
||||
|
||||
/// Connect to fprintd and get the default device.
|
||||
fn init_device(&mut self) {
|
||||
let manager = match gio::DBusProxy::for_bus_sync(
|
||||
gio::BusType::System,
|
||||
gio::DBusProxyFlags::NONE,
|
||||
None,
|
||||
FPRINTD_BUS_NAME,
|
||||
FPRINTD_MANAGER_PATH,
|
||||
FPRINTD_MANAGER_IFACE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::debug!("fprintd manager not available: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Call GetDefaultDevice
|
||||
let result = match manager.call_sync(
|
||||
"GetDefaultDevice",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::debug!("fprintd GetDefaultDevice failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Extract device path from variant tuple
|
||||
let device_path: String = result.child_get::<String>(0);
|
||||
if device_path.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match gio::DBusProxy::for_bus_sync(
|
||||
gio::BusType::System,
|
||||
gio::DBusProxyFlags::NONE,
|
||||
None,
|
||||
FPRINTD_BUS_NAME,
|
||||
&device_path,
|
||||
FPRINTD_DEVICE_IFACE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(proxy) => {
|
||||
self.device_proxy = Some(proxy);
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("fprintd device proxy failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if fprintd is available and the user has enrolled fingerprints.
|
||||
pub fn is_available(&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_sync(
|
||||
"ListEnrolledFingers",
|
||||
Some(&args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(result) => {
|
||||
// Result is a tuple of (array of strings)
|
||||
let fingers: Vec<String> = result.child_get::<Vec<String>>(0);
|
||||
!fingers.is_empty()
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the listener is currently running.
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.running
|
||||
}
|
||||
|
||||
/// Start listening for fingerprint verification.
|
||||
pub fn start<F, G>(&mut self, username: &str, on_success: F, on_failure: G)
|
||||
where
|
||||
F: Fn() + 'static,
|
||||
G: Fn() + 'static,
|
||||
{
|
||||
let proxy = match &self.device_proxy {
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
self.on_success = Some(Box::new(on_success));
|
||||
self.on_failure = Some(Box::new(on_failure));
|
||||
|
||||
// Claim the device
|
||||
let args = glib::Variant::from((&username,));
|
||||
if let Err(e) = proxy.call_sync(
|
||||
"Claim",
|
||||
Some(&args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
log::error!("Failed to claim fingerprint device: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start verification
|
||||
let start_args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy.call_sync(
|
||||
"VerifyStart",
|
||||
Some(&start_args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
log::error!("Failed to start fingerprint verification: {e}");
|
||||
let _ = proxy.call_sync(
|
||||
"Release",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self.running = true;
|
||||
|
||||
// Note: Signal handling is set up by the caller via connect_g_signal()
|
||||
// because FingerprintListener is not an Rc and the g-signal callback
|
||||
// needs to reference mutable state. The caller (lockscreen.rs) will
|
||||
// connect the proxy's "g-signal" and call on_verify_status().
|
||||
}
|
||||
|
||||
/// Process a VerifyStatus signal from fprintd.
|
||||
pub fn on_verify_status(&mut self, status: &str, done: bool) {
|
||||
if !self.running {
|
||||
return;
|
||||
}
|
||||
|
||||
if status == "verify-match" {
|
||||
if let Some(ref cb) = self.on_success {
|
||||
cb();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if RETRY_STATUSES.contains(&status) {
|
||||
if done {
|
||||
self.restart_verify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if status == "verify-no-match" {
|
||||
self.failed_attempts += 1;
|
||||
if let Some(ref cb) = self.on_failure {
|
||||
cb();
|
||||
}
|
||||
if self.failed_attempts >= MAX_FP_ATTEMPTS {
|
||||
log::warn!("Fingerprint max attempts ({MAX_FP_ATTEMPTS}) reached, stopping");
|
||||
self.stop();
|
||||
return;
|
||||
}
|
||||
if done {
|
||||
self.restart_verify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!("Unhandled fprintd status: {status}");
|
||||
}
|
||||
|
||||
/// Restart fingerprint verification after a completed attempt.
|
||||
fn restart_verify(&self) {
|
||||
if let Some(ref proxy) = self.device_proxy {
|
||||
let args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy.call_sync(
|
||||
"VerifyStart",
|
||||
Some(&args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
log::error!("Failed to restart fingerprint verification: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop listening and release the device.
|
||||
pub fn stop(&mut self) {
|
||||
if !self.running {
|
||||
return;
|
||||
}
|
||||
self.running = false;
|
||||
|
||||
if let Some(ref proxy) = self.device_proxy {
|
||||
if let Some(id) = self.signal_id.take() {
|
||||
proxy.disconnect(id);
|
||||
}
|
||||
let _ = proxy.call_sync(
|
||||
"VerifyStop",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
let _ = proxy.call_sync(
|
||||
"Release",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the device proxy for signal connection.
|
||||
pub fn device_proxy(&self) -> Option<&gio::DBusProxy> {
|
||||
self.device_proxy.as_ref()
|
||||
}
|
||||
|
||||
/// Store the signal handler ID for cleanup.
|
||||
pub fn set_signal_id(&mut self, id: glib::SignalHandlerId) {
|
||||
self.signal_id = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retry_statuses_are_defined() {
|
||||
assert!(RETRY_STATUSES.contains(&"verify-swipe-too-short"));
|
||||
assert!(RETRY_STATUSES.contains(&"verify-finger-not-centered"));
|
||||
assert!(!RETRY_STATUSES.contains(&"verify-match"));
|
||||
assert!(!RETRY_STATUSES.contains(&"verify-no-match"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_attempts_constant() {
|
||||
assert_eq!(MAX_FP_ATTEMPTS, 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user