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.
338 lines
9.8 KiB
Rust
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);
|
|
}
|
|
}
|