moonlock/src/fingerprint.rs
nevaforget 17f8930ff7 fix: security and correctness audit fixes (v0.4.1)
PAM conv callback: check msg_style (password only for ECHO_OFF),
handle strdup OOM with proper cleanup, null-check PAM handle.

Fingerprint: self-wire D-Bus g-signal in start() via Rc<RefCell<>>
and connect_local — VerifyStatus signals are now actually dispatched.
VerifyStop before VerifyStart in restart_verify.

Lockscreen: password entry stays active after faillock threshold
(PAM decides lockout, not UI), use Zeroizing<String> from GTK entry.

Release builds exit(1) without ext-session-lock-v1 support.

Config: fingerprint_enabled as Option<bool> so empty user config
does not override system config.

Dead code: remove unused i18n strings and fingerprint accessors,
parameterize faillock_warning max_attempts.
2026-03-28 00:06:27 +01:00

338 lines
9.8 KiB
Rust

// ABOUTME: fprintd D-Bus integration for fingerprint authentication.
// ABOUTME: Provides FingerprintListener that connects to fprintd via Gio.DBusProxy.
use gio::prelude::*;
use gtk4::gio;
use std::cell::RefCell;
use std::rc::Rc;
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,
}
}
/// Start listening for fingerprint verification.
/// Connects the D-Bus g-signal handler internally. The `listener` parameter
/// must be the same `Rc<RefCell<FingerprintListener>>` that owns `self`.
pub fn start<F, G>(
listener: &Rc<RefCell<FingerprintListener>>,
username: &str,
on_success: F,
on_failure: G,
) where
F: Fn() + 'static,
G: Fn() + 'static,
{
let proxy = {
let inner = listener.borrow();
match inner.device_proxy.clone() {
Some(p) => p,
None => return,
}
};
{
let mut inner = listener.borrow_mut();
inner.on_success = Some(Box::new(on_success));
inner.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;
}
// Connect the g-signal handler on the proxy to dispatch VerifyStatus
let listener_weak = Rc::downgrade(listener);
let signal_id = proxy.connect_local("g-signal", false, move |values| {
// g-signal arguments: (proxy, sender_name, signal_name, parameters)
let signal_name: String = match values[2].get() {
Ok(v) => v,
Err(_) => return None,
};
if signal_name.as_str() != "VerifyStatus" {
return None;
}
let params = match values[3].get::<glib::Variant>() {
Ok(v) => v,
Err(_) => return None,
};
let status = params
.child_value(0)
.get::<String>()
.unwrap_or_default();
let done = params
.child_value(1)
.get::<bool>()
.unwrap_or(false);
if let Some(listener_rc) = listener_weak.upgrade() {
listener_rc.borrow_mut().on_verify_status(&status, done);
}
None
});
let mut inner = listener.borrow_mut();
inner.signal_id = Some(signal_id);
inner.running = true;
}
/// 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 {
// VerifyStop before VerifyStart to avoid D-Bus errors
let _ = proxy.call_sync(
"VerifyStop",
None,
gio::DBusCallFlags::NONE,
-1,
gio::Cancellable::NONE,
);
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,
);
}
}
}
#[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);
}
}