diff --git a/Cargo.lock b/Cargo.lock index e3e3183..09f923b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,7 +575,7 @@ dependencies = [ [[package]] name = "moonlock" -version = "0.6.5" +version = "0.6.7" dependencies = [ "gdk-pixbuf", "gdk4", diff --git a/Cargo.toml b/Cargo.toml index 6b70fee..e776965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/fingerprint.rs b/src/fingerprint.rs index cf80f77..080c3cb 100644 --- a/src/fingerprint.rs +++ b/src/fingerprint.rs @@ -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, diff --git a/src/i18n.rs b/src/i18n.rs index 77284bf..d4d3d99 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -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 { 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 + } }