fix: audit fixes — fprintd path validation, progressive faillock warning (v0.6.7)
All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 1s

- Validate fprintd device path prefix (/net/reactivated/Fprint/Device/) before
  creating D-Bus proxy (prevents use of unexpected object paths)
- faillock_warning now warns at remaining <= 2 attempts (not just == 1), improving
  UX for higher max_attempts configurations
This commit is contained in:
nevaforget 2026-03-30 16:08:59 +02:00
parent af5b7c8912
commit 59c509dcbb
4 changed files with 26 additions and 7 deletions

2
Cargo.lock generated
View File

@ -575,7 +575,7 @@ dependencies = [
[[package]]
name = "moonlock"
version = "0.6.5"
version = "0.6.7"
dependencies = [
"gdk-pixbuf",
"gdk4",

View File

@ -1,6 +1,6 @@
[package]
name = "moonlock"
version = "0.6.6"
version = "0.6.7"
edition = "2024"
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
license = "MIT"

View File

@ -13,6 +13,7 @@ const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device";
const MAX_FP_ATTEMPTS: u32 = 10;
const DBUS_TIMEOUT_MS: i32 = 3000;
const FPRINTD_DEVICE_PREFIX: &str = "/net/reactivated/Fprint/Device/";
/// Retry-able statuses — finger not read properly, try again.
const RETRY_STATUSES: &[&str] = &[
@ -99,6 +100,10 @@ impl FingerprintListener {
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,

View File

@ -100,11 +100,13 @@ pub fn load_strings(locale: Option<&str>) -> &'static Strings {
match locale { "de" => &STRINGS_DE, _ => &STRINGS_EN }
}
/// Returns a warning when the user is one attempt away from lockout.
/// Returns a warning when the user is close to lockout (2 or fewer attempts remaining).
/// Caller is responsible for handling the locked state (count >= max_attempts).
pub fn faillock_warning(attempt_count: u32, max_attempts: u32, strings: &Strings) -> Option<String> {
let remaining = max_attempts.saturating_sub(attempt_count);
if remaining == 1 { return Some(strings.faillock_attempts_remaining.replace("{n}", &remaining.to_string())); }
if remaining > 0 && remaining <= 2 {
return Some(strings.faillock_attempts_remaining.replace("{n}", &remaining.to_string()));
}
None
}
@ -138,7 +140,7 @@ mod tests {
}
#[test] fn faillock_zero() { assert!(faillock_warning(0, 3, load_strings(Some("en"))).is_none()); }
#[test] fn faillock_one() { assert!(faillock_warning(1, 3, load_strings(Some("en"))).is_none()); }
#[test] fn faillock_one() { assert!(faillock_warning(1, 3, load_strings(Some("en"))).is_some()); }
#[test] fn faillock_two() { assert!(faillock_warning(2, 3, load_strings(Some("en"))).is_some()); }
#[test] fn faillock_three() { assert!(faillock_warning(3, 3, load_strings(Some("en"))).is_none()); }
@ -150,11 +152,23 @@ mod tests {
let strings = load_strings(Some("en"));
for count in 0..max {
let result = faillock_warning(count, max, strings);
if max - count == 1 {
assert!(result.is_some(), "should warn at count={count}");
let remaining = max - count;
if remaining <= 2 {
assert!(result.is_some(), "should warn at count={count} (remaining={remaining})");
} else {
assert!(result.is_none(), "should not warn at count={count}");
}
}
}
#[test]
fn faillock_warns_progressively_with_higher_max() {
let strings = load_strings(Some("en"));
// With max=5: warn at count 3 (rem=2) and count 4 (rem=1), not at 0-2
assert!(faillock_warning(0, 5, strings).is_none());
assert!(faillock_warning(2, 5, strings).is_none());
assert!(faillock_warning(3, 5, strings).is_some());
assert!(faillock_warning(4, 5, strings).is_some());
assert!(faillock_warning(5, 5, strings).is_none()); // at max, caller handles lockout
}
}