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.
This commit is contained in:
2026-03-28 00:06:27 +01:00
parent 64f032cd9a
commit 17f8930ff7
8 changed files with 201 additions and 70 deletions
+26 -9
View File
@@ -10,6 +10,8 @@ use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use zeroize::Zeroizing;
use crate::auth;
use crate::config::Config;
use crate::fingerprint::FingerprintListener;
@@ -24,6 +26,7 @@ const FAILLOCK_MAX_ATTEMPTS: u32 = 3;
struct LockscreenState {
failed_attempts: u32,
fp_listener: FingerprintListener,
fp_listener_rc: Option<Rc<RefCell<FingerprintListener>>>,
}
/// Create a lockscreen window for a single monitor.
@@ -54,6 +57,7 @@ pub fn create_lockscreen_window(
let state = Rc::new(RefCell::new(LockscreenState {
failed_attempts: 0,
fp_listener,
fp_listener_rc: None,
}));
// Root overlay for background + centered content
@@ -205,7 +209,7 @@ pub fn create_lockscreen_window(
#[weak]
password_entry,
move |entry| {
let password = entry.text().to_string();
let password = Zeroizing::new(entry.text().to_string());
if password.is_empty() {
return;
}
@@ -223,14 +227,18 @@ pub fn create_lockscreen_window(
password_entry,
async move {
let user = username.clone();
let pass = password.clone();
let pass = Zeroizing::new((*password).clone());
let result = gio::spawn_blocking(move || {
auth::authenticate(&user, &pass)
}).await;
match result {
Ok(true) => {
state.borrow_mut().fp_listener.stop();
let s = state.borrow();
if let Some(ref fp_rc) = s.fp_listener_rc {
fp_rc.borrow_mut().stop();
}
drop(s);
unlock_cb();
}
_ => {
@@ -241,13 +249,15 @@ pub fn create_lockscreen_window(
password_entry.set_text("");
if count >= FAILLOCK_MAX_ATTEMPTS {
// Show warning but keep entry active — PAM decides lockout
error_label.set_text(strings.faillock_locked);
error_label.set_visible(true);
password_entry.set_sensitive(false);
password_entry.set_sensitive(true);
password_entry.grab_focus();
} else {
password_entry.set_sensitive(true);
password_entry.grab_focus();
if let Some(warning) = faillock_warning(count, strings) {
if let Some(warning) = faillock_warning(count, FAILLOCK_MAX_ATTEMPTS, strings) {
error_label.set_text(&warning);
} else {
error_label.set_text(strings.wrong_password);
@@ -325,10 +335,17 @@ pub fn create_lockscreen_window(
));
};
state
.borrow_mut()
.fp_listener
.start(&user.username, on_success, on_failure);
// Extract the fp_listener into its own Rc<RefCell<>> for signal self-wiring
let fp_rc = {
let mut s = state.borrow_mut();
let listener = std::mem::replace(&mut s.fp_listener, FingerprintListener::new());
Rc::new(RefCell::new(listener))
};
FingerprintListener::start(&fp_rc, &user.username, on_success, on_failure);
// Store back the Rc reference for stop() on unlock
state.borrow_mut().fp_listener_rc = Some(fp_rc);
}
// Fade-in on map