fix: audit fixes — D-Bus sender validation, fp lifecycle, multi-monitor caching (v0.6.0)
Close the only exploitable auth bypass: validate VerifyStatus signal sender against fprintd's unique bus name. Fix fingerprint D-Bus lifecycle so devices are properly released on verify-match and async restarts check the running flag between awaits. Security: num_msg guard in PAM callback, symlink rejection for background_path, peek icon disabled, TOML parse errors logged, panic hook before logging. Performance: blur and avatar textures cached across monitors, release profile with LTO/strip.
This commit is contained in:
parent
4026f6dafa
commit
d11b6e634e
14
CLAUDE.md
14
CLAUDE.md
@ -38,22 +38,24 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
||||
## Architektur
|
||||
|
||||
- `auth.rs` — PAM-Authentifizierung via Raw FFI (unsafe extern "C" conv callback, msg_style-aware, Zeroizing<Vec<u8>>)
|
||||
- `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, async fire-and-forget stop, on_exhausted callback after MAX_FP_ATTEMPTS
|
||||
- `fingerprint.rs` — fprintd D-Bus Listener, async init/claim/verify via gio futures, sender-validated signal handler, cleanup_dbus() für sauberen D-Bus-Lifecycle, running_flag für Race-Safety in async restarts, on_exhausted callback after MAX_FP_ATTEMPTS
|
||||
- `users.rs` — Aktuellen User via nix getuid, Avatar-Loading mit Symlink-Rejection
|
||||
- `power.rs` — Reboot/Shutdown via /usr/bin/systemctl
|
||||
- `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
|
||||
- `config.rs` — TOML-Config (background_path, background_blur, fingerprint_enabled als Option<bool>) + Wallpaper-Fallback
|
||||
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking, FP-Label/Start separat verdrahtet, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode
|
||||
- `main.rs` — Entry Point, Panic-Hook, Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor, systemd-Journal-Logging, async fprintd-Init nach window.present()
|
||||
- `config.rs` — TOML-Config (background_path, background_blur, fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection für background_path + Parse-Error-Logging
|
||||
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking, FP-Label/Start separat verdrahtet, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode, Blur/Avatar-Cache für Multi-Monitor
|
||||
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor mit shared Blur/Avatar-Caches, systemd-Journal-Logging, async fprintd-Init nach window.present()
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- ext-session-lock-v1 garantiert: Compositor sperrt alle Surfaces bei lock()
|
||||
- Release-Build: Ohne ext-session-lock-v1 wird `exit(1)` aufgerufen — kein Fenster-Fallback
|
||||
- Panic-Hook: Bei Crash wird geloggt, aber NIEMALS unlock() aufgerufen — Screen bleibt schwarz
|
||||
- PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher
|
||||
- Panic-Hook: Bei Crash wird geloggt, aber NIEMALS unlock() aufgerufen — Screen bleibt schwarz. Hook wird vor Logging installiert.
|
||||
- PAM-Callback: msg_style-aware (Passwort nur bei PAM_PROMPT_ECHO_OFF), strdup-OOM-sicher, num_msg-Guard gegen negative Werte
|
||||
- fprintd: D-Bus Signal-Sender wird gegen fprintd's unique bus name validiert (Anti-Spoofing)
|
||||
- Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<Vec<u8>> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und CString werden nicht gezeroized — inhärente GTK/libc-Limitierung)
|
||||
- Root-Check: Exit mit Fehler wenn als root gestartet
|
||||
- Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv)
|
||||
- Kein Schließen per Escape/Alt-F4 — nur durch erfolgreiche PAM-Auth oder Fingerprint
|
||||
- Kein Peek-Icon am Passwortfeld (Shoulder-Surfing-Schutz)
|
||||
- GResource-Bundle: CSS/Assets in der Binary kompiliert
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -575,7 +575,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moonlock"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "moonlock"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||
license = "MIT"
|
||||
@ -26,3 +26,8 @@ tempfile = "3"
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.22"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
@ -2,6 +2,13 @@
|
||||
|
||||
Architectural and design decisions for Moonlock, in reverse chronological order.
|
||||
|
||||
## 2026-03-28 – Audit-driven security and lifecycle fixes (v0.6.0)
|
||||
|
||||
- **Who**: Nyx, Dom
|
||||
- **Why**: Triple audit (quality, performance, security) revealed a critical D-Bus signal spoofing vector, fingerprint lifecycle bugs, and multi-monitor performance issues.
|
||||
- **Tradeoffs**: `cleanup_dbus()` extraction adds a method but clarifies the stop/match ownership; `running_flag: Rc<Cell<bool>>` adds a field but prevents race between async restart and stop; sender validation adds a check per signal but closes the only known auth bypass.
|
||||
- **How**: (1) Validate D-Bus VerifyStatus sender against fprintd's unique bus name. (2) Extract `cleanup_dbus()` from `stop()`, call it on verify-match. (3) `Rc<Cell<bool>>` running flag checked after await in `restart_verify_async`. (4) Consistent 3s D-Bus timeouts. (5) Panic hook before logging. (6) Blur and avatar caches shared across monitors. (7) Peek icon disabled. (8) Symlink rejection for background_path. (9) TOML parse errors logged.
|
||||
|
||||
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||
|
||||
- **Who**: Nyx, Dom
|
||||
|
||||
@ -71,6 +71,10 @@ unsafe extern "C" fn pam_conv_callback(
|
||||
appdata_ptr: *mut libc::c_void,
|
||||
) -> libc::c_int {
|
||||
unsafe {
|
||||
if num_msg <= 0 {
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
// Safety: appdata_ptr was set to a valid *const CString in authenticate()
|
||||
let password = appdata_ptr as *const CString;
|
||||
if password.is_null() {
|
||||
|
||||
@ -49,10 +49,15 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
|
||||
let mut merged = Config::default();
|
||||
for path in paths {
|
||||
if let Ok(content) = fs::read_to_string(path) {
|
||||
if let Ok(parsed) = toml::from_str::<RawConfig>(&content) {
|
||||
if parsed.background_path.is_some() { merged.background_path = parsed.background_path; }
|
||||
if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; }
|
||||
if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; }
|
||||
match toml::from_str::<RawConfig>(&content) {
|
||||
Ok(parsed) => {
|
||||
if parsed.background_path.is_some() { merged.background_path = parsed.background_path; }
|
||||
if parsed.background_blur.is_some() { merged.background_blur = parsed.background_blur; }
|
||||
if let Some(fp) = parsed.fingerprint_enabled { merged.fingerprint_enabled = fp; }
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to parse {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,7 +71,7 @@ pub fn resolve_background_path(config: &Config) -> PathBuf {
|
||||
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> PathBuf {
|
||||
if let Some(ref bg) = config.background_path {
|
||||
let path = PathBuf::from(bg);
|
||||
if path.is_file() { return path; }
|
||||
if path.is_file() && !path.is_symlink() { return path; }
|
||||
}
|
||||
if moonarch_wallpaper.is_file() { return moonarch_wallpaper.to_path_buf(); }
|
||||
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
|
||||
@ -120,4 +125,24 @@ mod tests {
|
||||
let r = resolve_background_path_with(&c, Path::new("/nonexistent"));
|
||||
assert!(r.to_str().unwrap().contains("moonlock"));
|
||||
}
|
||||
#[test] fn toml_parse_error_returns_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("moonlock.toml");
|
||||
fs::write(&conf, "this is not valid toml {{{{").unwrap();
|
||||
let c = load_config(Some(&[conf]));
|
||||
assert!(c.fingerprint_enabled);
|
||||
assert!(c.background_path.is_none());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
#[test] fn symlink_rejected_for_background() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let real = dir.path().join("bg.jpg");
|
||||
let link = dir.path().join("link.jpg");
|
||||
fs::write(&real, "fake").unwrap();
|
||||
std::os::unix::fs::symlink(&real, &link).unwrap();
|
||||
let c = Config { background_path: Some(link.to_str().unwrap().to_string()), ..Config::default() };
|
||||
// Symlink should be rejected — falls through to moonarch wallpaper or gresource
|
||||
let r = resolve_background_path_with(&c, Path::new("/nonexistent"));
|
||||
assert_ne!(r, link);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
use gio::prelude::*;
|
||||
use gtk4::gio;
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint";
|
||||
@ -12,6 +12,7 @@ const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager";
|
||||
const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device";
|
||||
|
||||
const MAX_FP_ATTEMPTS: u32 = 10;
|
||||
const DBUS_TIMEOUT_MS: i32 = 3000;
|
||||
|
||||
/// Retry-able statuses — finger not read properly, try again.
|
||||
const RETRY_STATUSES: &[&str] = &[
|
||||
@ -26,6 +27,8 @@ pub struct FingerprintListener {
|
||||
device_proxy: Option<gio::DBusProxy>,
|
||||
signal_id: Option<glib::SignalHandlerId>,
|
||||
running: bool,
|
||||
/// Shared flag for async tasks to detect stop() between awaits.
|
||||
running_flag: Rc<Cell<bool>>,
|
||||
failed_attempts: u32,
|
||||
on_success: Option<Box<dyn Fn() + 'static>>,
|
||||
on_failure: Option<Box<dyn Fn() + 'static>>,
|
||||
@ -40,6 +43,7 @@ impl FingerprintListener {
|
||||
device_proxy: None,
|
||||
signal_id: None,
|
||||
running: false,
|
||||
running_flag: Rc::new(Cell::new(false)),
|
||||
failed_attempts: 0,
|
||||
on_success: None,
|
||||
on_failure: None,
|
||||
@ -68,7 +72,7 @@ impl FingerprintListener {
|
||||
|
||||
// Call GetDefaultDevice
|
||||
let result = match manager
|
||||
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
@ -118,7 +122,7 @@ impl FingerprintListener {
|
||||
|
||||
let args = glib::Variant::from((&username,));
|
||||
match proxy
|
||||
.call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
@ -168,7 +172,7 @@ impl FingerprintListener {
|
||||
// Claim the device
|
||||
let args = glib::Variant::from((&username,));
|
||||
if let Err(e) = proxy
|
||||
.call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to claim fingerprint device: {e}");
|
||||
@ -178,20 +182,34 @@ impl FingerprintListener {
|
||||
// Start verification
|
||||
let start_args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy
|
||||
.call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to start fingerprint verification: {e}");
|
||||
let _ = proxy
|
||||
.call_future("Release", None, gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture the unique bus name of fprintd for sender validation.
|
||||
// D-Bus signals carry the sender's unique name (e.g. ":1.42"), not the
|
||||
// well-known name. We validate this to prevent signal spoofing.
|
||||
let expected_sender = proxy.name_owner();
|
||||
|
||||
// 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 sender: String = match values[1].get() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
if expected_sender.as_ref().map(|s| s.as_str()) != Some(sender.as_str()) {
|
||||
log::warn!("Ignoring D-Bus signal from unexpected sender: {sender}");
|
||||
return None;
|
||||
}
|
||||
|
||||
let signal_name: String = match values[2].get() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
@ -224,6 +242,7 @@ impl FingerprintListener {
|
||||
let mut inner = listener.borrow_mut();
|
||||
inner.signal_id = Some(signal_id);
|
||||
inner.running = true;
|
||||
inner.running_flag.set(true);
|
||||
}
|
||||
|
||||
/// Process a VerifyStatus signal from fprintd.
|
||||
@ -233,7 +252,7 @@ impl FingerprintListener {
|
||||
}
|
||||
|
||||
if status == "verify-match" {
|
||||
self.running = false;
|
||||
self.cleanup_dbus();
|
||||
if let Some(ref cb) = self.on_success {
|
||||
cb();
|
||||
}
|
||||
@ -270,17 +289,22 @@ impl FingerprintListener {
|
||||
}
|
||||
|
||||
/// Restart fingerprint verification asynchronously after a completed attempt.
|
||||
/// Checks running_flag after VerifyStop to avoid restarting on a released device.
|
||||
fn restart_verify_async(&self) {
|
||||
if let Some(ref proxy) = self.device_proxy {
|
||||
let proxy = proxy.clone();
|
||||
let running = self.running_flag.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
// VerifyStop before VerifyStart to avoid D-Bus errors
|
||||
let _ = proxy
|
||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await;
|
||||
if !running.get() {
|
||||
return;
|
||||
}
|
||||
let args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy
|
||||
.call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, -1)
|
||||
.call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to restart fingerprint verification: {e}");
|
||||
@ -289,14 +313,12 @@ impl FingerprintListener {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop listening and release the device.
|
||||
/// Disconnect the signal handler and send VerifyStop + Release to fprintd.
|
||||
/// Signal disconnect is synchronous to prevent further callbacks.
|
||||
/// D-Bus cleanup (VerifyStop + Release) is fire-and-forget to avoid blocking the UI.
|
||||
pub fn stop(&mut self) {
|
||||
if !self.running {
|
||||
return;
|
||||
}
|
||||
/// D-Bus cleanup is fire-and-forget to avoid blocking the UI.
|
||||
fn cleanup_dbus(&mut self) {
|
||||
self.running = false;
|
||||
self.running_flag.set(false);
|
||||
|
||||
if let Some(ref proxy) = self.device_proxy {
|
||||
if let Some(id) = self.signal_id.take() {
|
||||
@ -305,15 +327,23 @@ impl FingerprintListener {
|
||||
let proxy = proxy.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let _ = proxy
|
||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, 3000)
|
||||
.call_future("VerifyStop", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await;
|
||||
let _ = proxy
|
||||
.call_future("Release", None, gio::DBusCallFlags::NONE, 3000)
|
||||
.call_future("Release", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop listening and release the device. Idempotent — safe to call multiple times.
|
||||
pub fn stop(&mut self) {
|
||||
if !self.running {
|
||||
return;
|
||||
}
|
||||
self.cleanup_dbus();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -335,21 +365,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn verify_match_sets_running_false_and_calls_success() {
|
||||
use std::cell::Cell;
|
||||
let called = Rc::new(Cell::new(false));
|
||||
let called_clone = called.clone();
|
||||
let mut listener = FingerprintListener::new();
|
||||
listener.running = true;
|
||||
listener.running_flag.set(true);
|
||||
listener.on_success = Some(Box::new(move || { called_clone.set(true); }));
|
||||
|
||||
listener.on_verify_status("verify-match", false);
|
||||
assert!(called.get());
|
||||
assert!(!listener.running);
|
||||
assert!(!listener.running_flag.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_no_match_calls_failure_and_stays_running() {
|
||||
use std::cell::Cell;
|
||||
let called = Rc::new(Cell::new(false));
|
||||
let called_clone = called.clone();
|
||||
let mut listener = FingerprintListener::new();
|
||||
@ -364,7 +394,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn max_attempts_stops_listener_and_calls_exhausted() {
|
||||
use std::cell::Cell;
|
||||
let exhausted = Rc::new(Cell::new(false));
|
||||
let exhausted_clone = exhausted.clone();
|
||||
let mut listener = FingerprintListener::new();
|
||||
@ -382,7 +411,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn not_running_ignores_signals() {
|
||||
use std::cell::Cell;
|
||||
let called = Rc::new(Cell::new(false));
|
||||
let called_clone = called.clone();
|
||||
let mut listener = FingerprintListener::new();
|
||||
|
||||
@ -41,11 +41,15 @@ struct LockscreenState {
|
||||
|
||||
/// Create a lockscreen window for a single monitor.
|
||||
/// Fingerprint is not initialized here — use `wire_fingerprint()` after async init.
|
||||
/// The `blur_cache` and `avatar_cache` are shared across monitors for multi-monitor
|
||||
/// setups, avoiding redundant GPU renders and SVG rasterizations.
|
||||
pub fn create_lockscreen_window(
|
||||
bg_texture: &gdk::Texture,
|
||||
config: &Config,
|
||||
app: >k::Application,
|
||||
unlock_callback: Rc<dyn Fn()>,
|
||||
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
avatar_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
) -> LockscreenHandles {
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
.application(app)
|
||||
@ -83,7 +87,7 @@ pub fn create_lockscreen_window(
|
||||
window.set_child(Some(&overlay));
|
||||
|
||||
// Background wallpaper
|
||||
let background = create_background_picture(bg_texture, config.background_blur);
|
||||
let background = create_background_picture(bg_texture, config.background_blur, blur_cache);
|
||||
overlay.set_child(Some(&background));
|
||||
|
||||
// Centered vertical box
|
||||
@ -109,12 +113,17 @@ pub fn create_lockscreen_window(
|
||||
avatar_frame.append(&avatar_image);
|
||||
login_box.append(&avatar_frame);
|
||||
|
||||
// Load avatar
|
||||
let avatar_path = users::get_avatar_path(&user.home, &user.username);
|
||||
if let Some(path) = avatar_path {
|
||||
set_avatar_from_file(&avatar_image, &path);
|
||||
// Load avatar — use shared cache to avoid redundant loading on multi-monitor setups.
|
||||
// The cache is populated by the first monitor and reused by subsequent ones.
|
||||
if let Some(ref cached) = *avatar_cache.borrow() {
|
||||
avatar_image.set_paintable(Some(cached));
|
||||
} else {
|
||||
set_default_avatar(&avatar_image, &window);
|
||||
let avatar_path = users::get_avatar_path(&user.home, &user.username);
|
||||
if let Some(path) = avatar_path {
|
||||
set_avatar_from_file(&avatar_image, &path, avatar_cache);
|
||||
} else {
|
||||
set_default_avatar(&avatar_image, &window, avatar_cache);
|
||||
}
|
||||
}
|
||||
|
||||
// Username label
|
||||
@ -125,7 +134,7 @@ pub fn create_lockscreen_window(
|
||||
// Password entry
|
||||
let password_entry = gtk::PasswordEntry::builder()
|
||||
.placeholder_text(strings.password_placeholder)
|
||||
.show_peek_icon(true)
|
||||
.show_peek_icon(false)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
password_entry.add_css_class("password-entry");
|
||||
@ -361,12 +370,18 @@ pub fn start_fingerprint(
|
||||
let fp_label_fail = handles.fp_label.clone();
|
||||
let unlock_cb_fp = handles.unlock_callback.clone();
|
||||
|
||||
let fp_rc_success = fp_rc.clone();
|
||||
let on_success = move || {
|
||||
let label = fp_label_success.clone();
|
||||
let cb = unlock_cb_fp.clone();
|
||||
let fp = fp_rc_success.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
label.set_text(load_strings(None).fingerprint_success);
|
||||
let strings = load_strings(None);
|
||||
label.set_text(strings.fingerprint_success);
|
||||
label.add_css_class("success");
|
||||
// stop() is idempotent — cleanup_dbus() already ran inside on_verify_status,
|
||||
// but this mirrors the PAM success path for defense-in-depth.
|
||||
fp.borrow_mut().stop();
|
||||
cb();
|
||||
});
|
||||
};
|
||||
@ -434,7 +449,13 @@ pub fn load_background_texture(bg_path: &Path) -> gdk::Texture {
|
||||
/// When `blur_radius` is `Some(sigma)` with sigma > 0, blur is applied via GPU
|
||||
/// (GskBlurNode). The blur is rendered to a concrete texture on `realize` (when
|
||||
/// the GPU renderer is available), avoiding lazy-render artifacts.
|
||||
fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -> gtk::Picture {
|
||||
/// The `blur_cache` is shared across monitors — the first to realize renders the
|
||||
/// blur, subsequent monitors reuse the cached texture.
|
||||
fn create_background_picture(
|
||||
texture: &gdk::Texture,
|
||||
blur_radius: Option<f32>,
|
||||
blur_cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
) -> gtk::Picture {
|
||||
let background = gtk::Picture::for_paintable(texture);
|
||||
background.set_content_fit(gtk::ContentFit::Cover);
|
||||
background.set_hexpand(true);
|
||||
@ -443,9 +464,15 @@ fn create_background_picture(texture: &gdk::Texture, blur_radius: Option<f32>) -
|
||||
if let Some(sigma) = blur_radius {
|
||||
if sigma > 0.0 {
|
||||
let texture = texture.clone();
|
||||
let cache = blur_cache.clone();
|
||||
background.connect_realize(move |picture| {
|
||||
if let Some(ref cached) = *cache.borrow() {
|
||||
picture.set_paintable(Some(cached));
|
||||
return;
|
||||
}
|
||||
if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) {
|
||||
picture.set_paintable(Some(&blurred));
|
||||
*cache.borrow_mut() = Some(blurred);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -477,12 +504,17 @@ fn render_blurred_texture(
|
||||
Some(renderer.render_texture(&node, None))
|
||||
}
|
||||
|
||||
/// Load an image file and set it as the avatar.
|
||||
fn set_avatar_from_file(image: >k::Image, path: &Path) {
|
||||
/// Load an image file and set it as the avatar. Stores the texture in the cache.
|
||||
fn set_avatar_from_file(
|
||||
image: >k::Image,
|
||||
path: &Path,
|
||||
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
) {
|
||||
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
|
||||
Ok(pixbuf) => {
|
||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||
image.set_paintable(Some(&texture));
|
||||
*cache.borrow_mut() = Some(texture);
|
||||
}
|
||||
Err(_) => {
|
||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||
@ -491,7 +523,12 @@ fn set_avatar_from_file(image: >k::Image, path: &Path) {
|
||||
}
|
||||
|
||||
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
||||
fn set_default_avatar(image: >k::Image, window: >k::ApplicationWindow) {
|
||||
/// Stores the texture in the cache for reuse on additional monitors.
|
||||
fn set_default_avatar(
|
||||
image: >k::Image,
|
||||
window: >k::ApplicationWindow,
|
||||
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||
) {
|
||||
let resource_path = users::get_default_avatar_path();
|
||||
if let Ok(bytes) =
|
||||
gio::resources_lookup_data(&resource_path, gio::ResourceLookupFlags::NONE)
|
||||
@ -513,6 +550,7 @@ fn set_default_avatar(image: >k::Image, window: >k::ApplicationWindow) {
|
||||
if let Some(pixbuf) = loader.pixbuf() {
|
||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||
image.set_paintable(Some(&texture));
|
||||
*cache.borrow_mut() = Some(texture);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
27
src/main.rs
27
src/main.rs
@ -86,6 +86,10 @@ fn activate_with_session_lock(
|
||||
app_clone.quit();
|
||||
});
|
||||
|
||||
// Shared caches for multi-monitor — first monitor renders, rest reuse
|
||||
let blur_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
||||
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
||||
|
||||
// Create all monitor windows immediately — no D-Bus calls here
|
||||
let mut all_handles = Vec::new();
|
||||
let mut created_any = false;
|
||||
@ -99,6 +103,8 @@ fn activate_with_session_lock(
|
||||
config,
|
||||
app,
|
||||
unlock_callback.clone(),
|
||||
&blur_cache,
|
||||
&avatar_cache,
|
||||
);
|
||||
lock.assign_window_to_monitor(&handles.window, &monitor);
|
||||
handles.window.present();
|
||||
@ -160,11 +166,15 @@ fn activate_without_lock(
|
||||
app_clone.quit();
|
||||
});
|
||||
|
||||
let blur_cache = Rc::new(RefCell::new(None));
|
||||
let avatar_cache = Rc::new(RefCell::new(None));
|
||||
let handles = lockscreen::create_lockscreen_window(
|
||||
bg_texture,
|
||||
config,
|
||||
app,
|
||||
unlock_callback,
|
||||
&blur_cache,
|
||||
&avatar_cache,
|
||||
);
|
||||
handles.window.set_default_size(800, 600);
|
||||
handles.window.present();
|
||||
@ -176,10 +186,16 @@ fn activate_without_lock(
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
systemd_journal_logger::JournalLog::new()
|
||||
.unwrap()
|
||||
.install()
|
||||
.unwrap();
|
||||
match systemd_journal_logger::JournalLog::new() {
|
||||
Ok(logger) => {
|
||||
if let Err(e) = logger.install() {
|
||||
eprintln!("Failed to install journal logger: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create journal logger: {e}");
|
||||
}
|
||||
}
|
||||
log::set_max_level(log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
@ -195,6 +211,7 @@ fn install_panic_hook() {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
install_panic_hook();
|
||||
setup_logging();
|
||||
|
||||
// Root check — moonlock should not run as root
|
||||
@ -202,8 +219,6 @@ fn main() {
|
||||
log::error!("Moonlock should not run as root");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
install_panic_hook();
|
||||
log::info!("Moonlock starting");
|
||||
|
||||
// Register compiled GResources
|
||||
|
||||
@ -7,14 +7,12 @@ use std::process::Command;
|
||||
#[derive(Debug)]
|
||||
pub enum PowerError {
|
||||
CommandFailed { action: &'static str, message: String },
|
||||
Timeout { action: &'static str },
|
||||
}
|
||||
|
||||
impl fmt::Display for PowerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PowerError::CommandFailed { action, message } => write!(f, "{action} failed: {message}"),
|
||||
PowerError::Timeout { action } => write!(f, "{action} timed out"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,7 +42,6 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test] fn power_error_display() { assert_eq!(PowerError::CommandFailed { action: "reboot", message: "fail".into() }.to_string(), "reboot failed: fail"); }
|
||||
#[test] fn timeout_display() { assert_eq!(PowerError::Timeout { action: "shutdown" }.to_string(), "shutdown timed out"); }
|
||||
#[test] fn missing_binary() { assert!(run_command("test", "nonexistent-xyz", &[]).is_err()); }
|
||||
#[test] fn nonzero_exit() { assert!(run_command("test", "false", &[]).is_err()); }
|
||||
#[test] fn success() { assert!(run_command("test", "true", &[]).is_ok()); }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user