All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Third triple audit (quality, performance, security). Key fixes: - Blur padding offset: texture at (-pad,-pad) prevents edge darkening on all sides - Wallpaper loads after lock.lock() — disk I/O no longer delays lock acquisition - begin_verification disconnects old signal handler before registering new one - resume_async resets failed_attempts to prevent premature exhaustion - Unknown VerifyStatus with done=true triggers restart instead of hanging - symlink_metadata() replaces separate is_file()+is_symlink() (TOCTOU) - faillock_warning dead code removed, blur sigma clamped to [0,100] - Redundant Zeroizing<Vec<u8>> removed, on_verify_status restricted to pub(crate) - Warn logging for non-UTF-8 GECOS and avatar path errors - Default impl for FingerprintListener, 3 new tests (47 total)
242 lines
7.3 KiB
Rust
242 lines
7.3 KiB
Rust
// ABOUTME: Entry point for Moonlock — secure Wayland lockscreen.
|
|
// ABOUTME: Sets up GTK Application, ext-session-lock-v1, Panic-Hook, and multi-monitor windows.
|
|
|
|
mod auth;
|
|
mod config;
|
|
mod fingerprint;
|
|
mod i18n;
|
|
mod lockscreen;
|
|
mod power;
|
|
mod users;
|
|
|
|
use gdk4 as gdk;
|
|
use gtk4::prelude::*;
|
|
use gtk4::{self as gtk, gio};
|
|
use gtk4_session_lock;
|
|
use std::cell::{Cell, RefCell};
|
|
use std::rc::Rc;
|
|
|
|
use crate::fingerprint::FingerprintListener;
|
|
|
|
fn load_css(display: &gdk::Display) {
|
|
let css_provider = gtk::CssProvider::new();
|
|
css_provider.load_from_resource("/dev/moonarch/moonlock/style.css");
|
|
gtk::style_context_add_provider_for_display(
|
|
display,
|
|
&css_provider,
|
|
gtk::STYLE_PROVIDER_PRIORITY_USER,
|
|
);
|
|
}
|
|
|
|
fn activate(app: >k::Application) {
|
|
let display = match gdk::Display::default() {
|
|
Some(d) => d,
|
|
None => {
|
|
log::error!("No display available — cannot start lockscreen UI");
|
|
return;
|
|
}
|
|
};
|
|
|
|
load_css(&display);
|
|
|
|
let config = config::load_config(None);
|
|
|
|
if gtk4_session_lock::is_supported() {
|
|
activate_with_session_lock(app, &display, &config);
|
|
} else {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
log::warn!("ext-session-lock-v1 not supported — running in development mode");
|
|
activate_without_lock(app, &config);
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
log::error!("ext-session-lock-v1 not supported — refusing to run without session lock");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn activate_with_session_lock(
|
|
app: >k::Application,
|
|
display: &gdk::Display,
|
|
config: &config::Config,
|
|
) {
|
|
let lock = gtk4_session_lock::Instance::new();
|
|
lock.lock();
|
|
|
|
// Load wallpaper AFTER lock — disk I/O must not delay the lock acquisition
|
|
let bg_texture = config::resolve_background_path(config)
|
|
.and_then(|path| lockscreen::load_background_texture(&path));
|
|
|
|
let monitors = display.monitors();
|
|
|
|
// Shared unlock callback — unlocks session and quits.
|
|
// Guard prevents double-unlock if PAM and fingerprint succeed simultaneously.
|
|
let lock_clone = lock.clone();
|
|
let app_clone = app.clone();
|
|
let already_unlocked = Rc::new(Cell::new(false));
|
|
let au = already_unlocked.clone();
|
|
let unlock_callback: Rc<dyn Fn()> = Rc::new(move || {
|
|
if au.get() {
|
|
log::debug!("Unlock already triggered, ignoring duplicate");
|
|
return;
|
|
}
|
|
au.set(true);
|
|
lock_clone.unlock();
|
|
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;
|
|
for i in 0..monitors.n_items() {
|
|
if let Some(monitor) = monitors
|
|
.item(i)
|
|
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
|
{
|
|
let handles = lockscreen::create_lockscreen_window(
|
|
bg_texture.as_ref(),
|
|
config,
|
|
app,
|
|
unlock_callback.clone(),
|
|
&blur_cache,
|
|
&avatar_cache,
|
|
);
|
|
lock.assign_window_to_monitor(&handles.window, &monitor);
|
|
handles.window.present();
|
|
all_handles.push(handles);
|
|
created_any = true;
|
|
}
|
|
}
|
|
|
|
if !created_any {
|
|
log::error!("No lockscreen windows created — screen stays locked (compositor policy)");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Async fprintd initialization — runs after windows are visible
|
|
if config.fingerprint_enabled {
|
|
init_fingerprint_async(all_handles);
|
|
}
|
|
}
|
|
|
|
/// Initialize fprintd asynchronously after windows are visible.
|
|
/// Uses a single FingerprintListener shared across all monitors —
|
|
/// only the first monitor's handles get the fingerprint UI wired up.
|
|
fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
|
|
glib::spawn_future_local(async move {
|
|
let mut listener = FingerprintListener::new();
|
|
listener.init_async().await;
|
|
|
|
// Use the first monitor's username to check enrollment
|
|
let username = &all_handles[0].username;
|
|
if username.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if !listener.is_available_async(username).await {
|
|
log::debug!("fprintd not available or no enrolled fingers");
|
|
return;
|
|
}
|
|
|
|
let fp_rc = Rc::new(RefCell::new(listener));
|
|
|
|
// Show fingerprint label on all monitors
|
|
for handles in &all_handles {
|
|
lockscreen::show_fingerprint_label(handles, &fp_rc);
|
|
}
|
|
|
|
// Start verification listener on the first monitor only
|
|
lockscreen::start_fingerprint(&all_handles[0], &fp_rc);
|
|
});
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
fn activate_without_lock(
|
|
app: >k::Application,
|
|
config: &config::Config,
|
|
) {
|
|
let bg_texture = config::resolve_background_path(config)
|
|
.and_then(|path| lockscreen::load_background_texture(&path));
|
|
|
|
let app_clone = app.clone();
|
|
let unlock_callback: Rc<dyn Fn()> = Rc::new(move || {
|
|
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.as_ref(),
|
|
config,
|
|
app,
|
|
unlock_callback,
|
|
&blur_cache,
|
|
&avatar_cache,
|
|
);
|
|
handles.window.set_default_size(800, 600);
|
|
handles.window.present();
|
|
|
|
// Async fprintd initialization for development mode
|
|
if config.fingerprint_enabled {
|
|
init_fingerprint_async(vec![handles]);
|
|
}
|
|
}
|
|
|
|
fn setup_logging() {
|
|
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}");
|
|
}
|
|
}
|
|
let level = if std::env::var("MOONLOCK_DEBUG").is_ok() {
|
|
log::LevelFilter::Debug
|
|
} else {
|
|
log::LevelFilter::Info
|
|
};
|
|
log::set_max_level(level);
|
|
}
|
|
|
|
fn install_panic_hook() {
|
|
// Install a panic hook BEFORE starting the app.
|
|
// On panic, we log but NEVER unlock. The compositor's ext-session-lock-v1
|
|
// policy keeps the screen locked when the client crashes.
|
|
let default_hook = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
log::error!("PANIC — screen stays locked (compositor policy): {info}");
|
|
default_hook(info);
|
|
}));
|
|
}
|
|
|
|
fn main() {
|
|
install_panic_hook();
|
|
setup_logging();
|
|
|
|
// Root check — moonlock should not run as root
|
|
if nix::unistd::getuid().is_root() {
|
|
log::error!("Moonlock should not run as root");
|
|
std::process::exit(1);
|
|
}
|
|
log::info!("Moonlock starting");
|
|
|
|
// Register compiled GResources
|
|
gio::resources_register_include!("moonlock.gresource").expect("Failed to register resources");
|
|
|
|
let app = gtk::Application::builder()
|
|
.application_id("dev.moonarch.moonlock")
|
|
.build();
|
|
|
|
app.connect_activate(activate);
|
|
app.run();
|
|
}
|