Rewrite moonlock from Python to Rust (v0.4.0)

Complete rewrite of the Wayland lockscreen from Python/PyGObject to
Rust/gtk4-rs for memory safety in security-critical PAM code and
consistency with the moonset/moongreet Rust ecosystem.

Modules: main, lockscreen, auth (PAM FFI), fingerprint (fprintd D-Bus),
config, i18n, users, power. 37 unit tests.

Security: PAM conversation callback with Zeroizing password, panic hook
that never unlocks, root check, ext-session-lock-v1 compositor policy,
absolute loginctl path, avatar symlink rejection.
This commit is contained in:
2026-03-27 23:09:54 +01:00
parent 7de3737a61
commit 817a9547ad
41 changed files with 3075 additions and 2264 deletions
+164
View File
@@ -0,0 +1,164 @@
// 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::path::PathBuf;
use std::rc::Rc;
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_APPLICATION,
);
}
fn activate(app: &gtk::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);
let bg_path = config::resolve_background_path(&config);
if gtk4_session_lock::is_supported() {
activate_with_session_lock(app, &display, &bg_path, &config);
} else {
log::warn!("ext-session-lock-v1 not supported — running in development mode");
activate_without_lock(app, &bg_path, &config);
}
}
fn activate_with_session_lock(
app: &gtk::Application,
display: &gdk::Display,
bg_path: &PathBuf,
config: &config::Config,
) {
let lock = gtk4_session_lock::Instance::new();
lock.lock();
let monitors = display.monitors();
// Shared unlock callback — unlocks session and quits
let lock_clone = lock.clone();
let app_clone = app.clone();
let unlock_callback: Rc<dyn Fn()> = Rc::new(move || {
lock_clone.unlock();
app_clone.quit();
});
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 window = lockscreen::create_lockscreen_window(
bg_path,
config,
app,
unlock_callback.clone(),
);
lock.assign_window_to_monitor(&window, &monitor);
window.present();
created_any = true;
}
}
if !created_any {
log::error!("No lockscreen windows created — screen stays locked (compositor policy)");
}
}
fn activate_without_lock(
app: &gtk::Application,
bg_path: &PathBuf,
config: &config::Config,
) {
let app_clone = app.clone();
let unlock_callback: Rc<dyn Fn()> = Rc::new(move || {
app_clone.quit();
});
let window = lockscreen::create_lockscreen_window(
bg_path,
config,
app,
unlock_callback,
);
window.set_default_size(800, 600);
window.present();
}
fn setup_logging() {
let mut builder = env_logger::Builder::from_default_env();
builder.filter_level(log::LevelFilter::Info);
let log_dir = PathBuf::from("/var/cache/moonlock");
if log_dir.is_dir() {
let log_file = log_dir.join("moonlock.log");
if let Ok(file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
{
builder.target(env_logger::Target::Pipe(Box::new(file)));
}
}
builder.init();
}
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() {
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);
}
install_panic_hook();
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();
}