Compare commits
No commits in common. "09e0d47a38c155369105dd3e72e99d5a6eee846a" and "58c076198f1ae95769f315604e5ff8441e72c167" have entirely different histories.
09e0d47a38
...
58c076198f
10
CLAUDE.md
10
CLAUDE.md
@ -38,13 +38,13 @@ 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, sync stop with 3s timeout, on_exhausted callback after MAX_FP_ATTEMPTS
|
||||
- `fingerprint.rs` — fprintd D-Bus Listener (Rc<RefCell<FingerprintListener>>, self-wiring g-signal via connect_local)
|
||||
- `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
|
||||
- `i18n.rs` — Locale-Erkennung und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
|
||||
- `config.rs` — TOML-Config (background_path, 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
|
||||
- `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()
|
||||
- `lockscreen.rs` — GTK4 UI, PAM-Auth via gio::spawn_blocking, Fingerprint-Indikator, Zeroizing<String> für Passwort, Power-Confirm
|
||||
- `main.rs` — Entry Point, Panic-Hook, Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor, systemd-Journal-Logging
|
||||
|
||||
## Sicherheit
|
||||
|
||||
@ -52,7 +52,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
||||
- 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
|
||||
- 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)
|
||||
- Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<Vec<u8>> im PAM-FFI-Layer
|
||||
- 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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "moonlock"
|
||||
version = "0.5.0"
|
||||
version = "0.4.2"
|
||||
edition = "2024"
|
||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||
license = "MIT"
|
||||
|
||||
@ -7,8 +7,8 @@ Part of the Moonarch ecosystem.
|
||||
|
||||
- **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash)
|
||||
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`)
|
||||
- **Fingerprint unlock** — fprintd D-Bus integration, async init (optional, window appears instantly)
|
||||
- **Multi-monitor** — Lockscreen on every monitor, single shared fingerprint listener
|
||||
- **Fingerprint unlock** — fprintd D-Bus integration (optional)
|
||||
- **Multi-monitor** — Lockscreen on every monitor
|
||||
- **i18n** — German and English (auto-detected)
|
||||
- **Faillock warning** — UI counter + system pam_faillock
|
||||
- **Panic safety** — Panic hook logs but never unlocks
|
||||
|
||||
@ -29,36 +29,35 @@ pub struct FingerprintListener {
|
||||
failed_attempts: u32,
|
||||
on_success: Option<Box<dyn Fn() + 'static>>,
|
||||
on_failure: Option<Box<dyn Fn() + 'static>>,
|
||||
on_exhausted: Option<Box<dyn Fn() + 'static>>,
|
||||
}
|
||||
|
||||
impl FingerprintListener {
|
||||
/// Create a lightweight FingerprintListener without any D-Bus calls.
|
||||
/// Call `init_async().await` afterwards to connect to fprintd.
|
||||
/// Create a new FingerprintListener.
|
||||
/// Connects to fprintd synchronously — call before creating GTK windows.
|
||||
pub fn new() -> Self {
|
||||
FingerprintListener {
|
||||
let mut listener = FingerprintListener {
|
||||
device_proxy: None,
|
||||
signal_id: None,
|
||||
running: false,
|
||||
failed_attempts: 0,
|
||||
on_success: None,
|
||||
on_failure: None,
|
||||
on_exhausted: None,
|
||||
}
|
||||
};
|
||||
listener.init_device();
|
||||
listener
|
||||
}
|
||||
|
||||
/// Connect to fprintd and get the default device asynchronously.
|
||||
pub async fn init_async(&mut self) {
|
||||
let manager = match gio::DBusProxy::for_bus_future(
|
||||
/// Connect to fprintd and get the default device.
|
||||
fn init_device(&mut self) {
|
||||
let manager = match gio::DBusProxy::for_bus_sync(
|
||||
gio::BusType::System,
|
||||
gio::DBusProxyFlags::NONE,
|
||||
None,
|
||||
FPRINTD_BUS_NAME,
|
||||
FPRINTD_MANAGER_PATH,
|
||||
FPRINTD_MANAGER_IFACE,
|
||||
)
|
||||
.await
|
||||
{
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::debug!("fprintd manager not available: {e}");
|
||||
@ -67,10 +66,13 @@ impl FingerprintListener {
|
||||
};
|
||||
|
||||
// Call GetDefaultDevice
|
||||
let result = match manager
|
||||
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, -1)
|
||||
.await
|
||||
{
|
||||
let result = match manager.call_sync(
|
||||
"GetDefaultDevice",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::debug!("fprintd GetDefaultDevice failed: {e}");
|
||||
@ -79,27 +81,20 @@ impl FingerprintListener {
|
||||
};
|
||||
|
||||
// Extract device path from variant tuple
|
||||
let device_path = match result.child_value(0).get::<String>() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
log::debug!("fprintd: unexpected GetDefaultDevice response type");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let device_path: String = result.child_get::<String>(0);
|
||||
if device_path.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match gio::DBusProxy::for_bus_future(
|
||||
match gio::DBusProxy::for_bus_sync(
|
||||
gio::BusType::System,
|
||||
gio::DBusProxyFlags::NONE,
|
||||
None,
|
||||
FPRINTD_BUS_NAME,
|
||||
&device_path,
|
||||
FPRINTD_DEVICE_IFACE,
|
||||
)
|
||||
.await
|
||||
{
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(proxy) => {
|
||||
self.device_proxy = Some(proxy);
|
||||
}
|
||||
@ -109,46 +104,41 @@ impl FingerprintListener {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if fprintd is available and the user has enrolled fingerprints (async).
|
||||
pub async fn is_available_async(&self, username: &str) -> bool {
|
||||
/// Check if fprintd is available and the user has enrolled fingerprints.
|
||||
pub fn is_available(&self, username: &str) -> bool {
|
||||
let proxy = match &self.device_proxy {
|
||||
Some(p) => p,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let args = glib::Variant::from((&username,));
|
||||
match proxy
|
||||
.call_future("ListEnrolledFingers", Some(&args), gio::DBusCallFlags::NONE, -1)
|
||||
.await
|
||||
{
|
||||
match proxy.call_sync(
|
||||
"ListEnrolledFingers",
|
||||
Some(&args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(result) => {
|
||||
// Result is a tuple of (array of strings)
|
||||
match result.child_value(0).get::<Vec<String>>() {
|
||||
Some(fingers) => !fingers.is_empty(),
|
||||
None => {
|
||||
log::debug!("fprintd: unexpected ListEnrolledFingers response type");
|
||||
false
|
||||
}
|
||||
}
|
||||
let fingers: Vec<String> = result.child_get::<Vec<String>>(0);
|
||||
!fingers.is_empty()
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start listening for fingerprint verification.
|
||||
/// Claims the device and starts verification using async D-Bus calls.
|
||||
/// Connects the D-Bus g-signal handler internally. The `listener` parameter
|
||||
/// must be the same `Rc<RefCell<FingerprintListener>>` that owns `self`.
|
||||
pub async fn start_async<F, G, H>(
|
||||
pub fn start<F, G>(
|
||||
listener: &Rc<RefCell<FingerprintListener>>,
|
||||
username: &str,
|
||||
on_success: F,
|
||||
on_failure: G,
|
||||
on_exhausted: H,
|
||||
) where
|
||||
F: Fn() + 'static,
|
||||
G: Fn() + 'static,
|
||||
H: Fn() + 'static,
|
||||
{
|
||||
let proxy = {
|
||||
let inner = listener.borrow();
|
||||
@ -162,29 +152,38 @@ impl FingerprintListener {
|
||||
let mut inner = listener.borrow_mut();
|
||||
inner.on_success = Some(Box::new(on_success));
|
||||
inner.on_failure = Some(Box::new(on_failure));
|
||||
inner.on_exhausted = Some(Box::new(on_exhausted));
|
||||
}
|
||||
|
||||
// Claim the device
|
||||
let args = glib::Variant::from((&username,));
|
||||
if let Err(e) = proxy
|
||||
.call_future("Claim", Some(&args), gio::DBusCallFlags::NONE, -1)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = proxy.call_sync(
|
||||
"Claim",
|
||||
Some(&args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
log::error!("Failed to claim fingerprint device: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start verification
|
||||
let start_args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy
|
||||
.call_future("VerifyStart", Some(&start_args), gio::DBusCallFlags::NONE, -1)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = proxy.call_sync(
|
||||
"VerifyStart",
|
||||
Some(&start_args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
log::error!("Failed to start fingerprint verification: {e}");
|
||||
let _ = proxy
|
||||
.call_future("Release", None, gio::DBusCallFlags::NONE, -1)
|
||||
.await;
|
||||
let _ = proxy.call_sync(
|
||||
"Release",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -233,7 +232,6 @@ impl FingerprintListener {
|
||||
}
|
||||
|
||||
if status == "verify-match" {
|
||||
self.running = false;
|
||||
if let Some(ref cb) = self.on_success {
|
||||
cb();
|
||||
}
|
||||
@ -242,26 +240,23 @@ impl FingerprintListener {
|
||||
|
||||
if RETRY_STATUSES.contains(&status) {
|
||||
if done {
|
||||
self.restart_verify_async();
|
||||
self.restart_verify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if status == "verify-no-match" {
|
||||
self.failed_attempts += 1;
|
||||
if self.failed_attempts >= MAX_FP_ATTEMPTS {
|
||||
log::warn!("Fingerprint max attempts ({MAX_FP_ATTEMPTS}) reached, stopping");
|
||||
if let Some(ref cb) = self.on_exhausted {
|
||||
cb();
|
||||
}
|
||||
self.stop();
|
||||
return;
|
||||
}
|
||||
if let Some(ref cb) = self.on_failure {
|
||||
cb();
|
||||
}
|
||||
if self.failed_attempts >= MAX_FP_ATTEMPTS {
|
||||
log::warn!("Fingerprint max attempts ({MAX_FP_ATTEMPTS}) reached, stopping");
|
||||
self.stop();
|
||||
return;
|
||||
}
|
||||
if done {
|
||||
self.restart_verify_async();
|
||||
self.restart_verify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -269,28 +264,31 @@ impl FingerprintListener {
|
||||
log::debug!("Unhandled fprintd status: {status}");
|
||||
}
|
||||
|
||||
/// Restart fingerprint verification asynchronously after a completed attempt.
|
||||
fn restart_verify_async(&self) {
|
||||
/// Restart fingerprint verification after a completed attempt.
|
||||
fn restart_verify(&self) {
|
||||
if let Some(ref proxy) = self.device_proxy {
|
||||
let proxy = proxy.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)
|
||||
.await;
|
||||
let args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy
|
||||
.call_future("VerifyStart", Some(&args), gio::DBusCallFlags::NONE, -1)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to restart fingerprint verification: {e}");
|
||||
}
|
||||
});
|
||||
// VerifyStop before VerifyStart to avoid D-Bus errors
|
||||
let _ = proxy.call_sync(
|
||||
"VerifyStop",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
let args = glib::Variant::from((&"any",));
|
||||
if let Err(e) = proxy.call_sync(
|
||||
"VerifyStart",
|
||||
Some(&args),
|
||||
gio::DBusCallFlags::NONE,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
log::error!("Failed to restart fingerprint verification: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop listening and release the device.
|
||||
/// Uses a short timeout (3s) to avoid blocking the UI indefinitely.
|
||||
pub fn stop(&mut self) {
|
||||
if !self.running {
|
||||
return;
|
||||
@ -305,14 +303,14 @@ impl FingerprintListener {
|
||||
"VerifyStop",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
3000,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
let _ = proxy.call_sync(
|
||||
"Release",
|
||||
None,
|
||||
gio::DBusCallFlags::NONE,
|
||||
3000,
|
||||
-1,
|
||||
gio::Cancellable::NONE,
|
||||
);
|
||||
}
|
||||
@ -336,64 +334,4 @@ mod tests {
|
||||
fn max_attempts_constant() {
|
||||
assert_eq!(MAX_FP_ATTEMPTS, 10);
|
||||
}
|
||||
|
||||
#[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.on_success = Some(Box::new(move || { called_clone.set(true); }));
|
||||
|
||||
listener.on_verify_status("verify-match", false);
|
||||
assert!(called.get());
|
||||
assert!(!listener.running);
|
||||
}
|
||||
|
||||
#[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();
|
||||
listener.running = true;
|
||||
listener.on_failure = Some(Box::new(move || { called_clone.set(true); }));
|
||||
|
||||
listener.on_verify_status("verify-no-match", false);
|
||||
assert!(called.get());
|
||||
assert!(listener.running);
|
||||
assert_eq!(listener.failed_attempts, 1);
|
||||
}
|
||||
|
||||
#[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();
|
||||
listener.running = true;
|
||||
listener.on_failure = Some(Box::new(|| {}));
|
||||
listener.on_exhausted = Some(Box::new(move || { exhausted_clone.set(true); }));
|
||||
|
||||
for _ in 0..MAX_FP_ATTEMPTS {
|
||||
listener.on_verify_status("verify-no-match", true);
|
||||
}
|
||||
assert!(!listener.running);
|
||||
assert!(exhausted.get());
|
||||
assert_eq!(listener.failed_attempts, MAX_FP_ATTEMPTS);
|
||||
}
|
||||
|
||||
#[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();
|
||||
listener.running = false;
|
||||
listener.on_success = Some(Box::new(move || { called_clone.set(true); }));
|
||||
|
||||
listener.on_verify_status("verify-match", false);
|
||||
assert!(!called.get());
|
||||
}
|
||||
}
|
||||
|
||||
11
src/i18n.rs
11
src/i18n.rs
@ -4,13 +4,9 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf";
|
||||
|
||||
/// Cached locale prefix — detected once, reused for all subsequent calls.
|
||||
static CACHED_LOCALE: OnceLock<String> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Strings {
|
||||
pub password_placeholder: &'static str,
|
||||
@ -90,11 +86,8 @@ pub fn detect_locale() -> String {
|
||||
}
|
||||
|
||||
pub fn load_strings(locale: Option<&str>) -> &'static Strings {
|
||||
let locale = match locale {
|
||||
Some(l) => l,
|
||||
None => CACHED_LOCALE.get_or_init(detect_locale),
|
||||
};
|
||||
match locale { "de" => &STRINGS_DE, _ => &STRINGS_EN }
|
||||
let locale = match locale { Some(l) => l.to_string(), None => detect_locale() };
|
||||
match locale.as_str() { "de" => &STRINGS_DE, _ => &STRINGS_EN }
|
||||
}
|
||||
|
||||
pub fn faillock_warning(attempt_count: u32, max_attempts: u32, strings: &Strings) -> Option<String> {
|
||||
|
||||
@ -19,33 +19,23 @@ use crate::i18n::{faillock_warning, load_strings, Strings};
|
||||
use crate::power::{self, PowerError};
|
||||
use crate::users;
|
||||
|
||||
/// Handles returned from create_lockscreen_window for post-creation wiring.
|
||||
pub struct LockscreenHandles {
|
||||
pub window: gtk::ApplicationWindow,
|
||||
pub fp_label: gtk::Label,
|
||||
pub password_entry: gtk::PasswordEntry,
|
||||
pub unlock_callback: Rc<dyn Fn()>,
|
||||
pub username: String,
|
||||
state: Rc<RefCell<LockscreenState>>,
|
||||
}
|
||||
|
||||
const AVATAR_SIZE: i32 = 128;
|
||||
const FAILLOCK_MAX_ATTEMPTS: u32 = 3;
|
||||
|
||||
/// Shared mutable state for the lockscreen.
|
||||
struct LockscreenState {
|
||||
failed_attempts: u32,
|
||||
fp_listener: FingerprintListener,
|
||||
fp_listener_rc: Option<Rc<RefCell<FingerprintListener>>>,
|
||||
}
|
||||
|
||||
/// Create a lockscreen window for a single monitor.
|
||||
/// Fingerprint is not initialized here — use `wire_fingerprint()` after async init.
|
||||
pub fn create_lockscreen_window(
|
||||
bg_path: &Path,
|
||||
_config: &Config,
|
||||
config: &Config,
|
||||
app: >k::Application,
|
||||
unlock_callback: Rc<dyn Fn()>,
|
||||
) -> LockscreenHandles {
|
||||
) -> gtk::ApplicationWindow {
|
||||
let window = gtk::ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.build();
|
||||
@ -56,24 +46,17 @@ pub fn create_lockscreen_window(
|
||||
Some(u) => u,
|
||||
None => {
|
||||
log::error!("Failed to get current user");
|
||||
let fp_label = gtk::Label::new(None);
|
||||
fp_label.set_visible(false);
|
||||
return LockscreenHandles {
|
||||
window,
|
||||
fp_label,
|
||||
password_entry: gtk::PasswordEntry::new(),
|
||||
unlock_callback,
|
||||
username: String::new(),
|
||||
state: Rc::new(RefCell::new(LockscreenState {
|
||||
failed_attempts: 0,
|
||||
fp_listener_rc: None,
|
||||
})),
|
||||
};
|
||||
return window;
|
||||
}
|
||||
};
|
||||
|
||||
let fp_listener = FingerprintListener::new();
|
||||
let fp_available = config.fingerprint_enabled
|
||||
&& fp_listener.is_available(&user.username);
|
||||
|
||||
let state = Rc::new(RefCell::new(LockscreenState {
|
||||
failed_attempts: 0,
|
||||
fp_listener,
|
||||
fp_listener_rc: None,
|
||||
}));
|
||||
|
||||
@ -136,10 +119,15 @@ pub fn create_lockscreen_window(
|
||||
error_label.set_visible(false);
|
||||
login_box.append(&error_label);
|
||||
|
||||
// Fingerprint label — hidden until async fprintd init completes
|
||||
// Fingerprint label
|
||||
let fp_label = gtk::Label::new(None);
|
||||
fp_label.add_css_class("fingerprint-label");
|
||||
fp_label.set_visible(false);
|
||||
if fp_available {
|
||||
fp_label.set_text(strings.fingerprint_prompt);
|
||||
fp_label.set_visible(true);
|
||||
} else {
|
||||
fp_label.set_visible(false);
|
||||
}
|
||||
login_box.append(&fp_label);
|
||||
|
||||
// Confirm box area (for power confirm)
|
||||
@ -239,8 +227,9 @@ pub fn create_lockscreen_window(
|
||||
password_entry,
|
||||
async move {
|
||||
let user = username.clone();
|
||||
let pass = Zeroizing::new((*password).clone());
|
||||
let result = gio::spawn_blocking(move || {
|
||||
auth::authenticate(&user, &password)
|
||||
auth::authenticate(&user, &pass)
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
@ -304,6 +293,61 @@ pub fn create_lockscreen_window(
|
||||
));
|
||||
window.add_controller(key_controller);
|
||||
|
||||
// Start fingerprint listener
|
||||
if fp_available {
|
||||
let unlock_cb_fp = unlock_callback.clone();
|
||||
let fp_label_success = fp_label.clone();
|
||||
let fp_label_fail = fp_label.clone();
|
||||
|
||||
let on_success = move || {
|
||||
let label = fp_label_success.clone();
|
||||
let cb = unlock_cb_fp.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
label.set_text(load_strings(None).fingerprint_success);
|
||||
label.add_css_class("success");
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
let on_failure = move || {
|
||||
let label = fp_label_fail.clone();
|
||||
glib::idle_add_local_once(clone!(
|
||||
#[weak]
|
||||
label,
|
||||
move || {
|
||||
let strings = load_strings(None);
|
||||
label.set_text(strings.fingerprint_failed);
|
||||
label.add_css_class("failed");
|
||||
// Reset after 2 seconds
|
||||
glib::timeout_add_local_once(
|
||||
std::time::Duration::from_secs(2),
|
||||
clone!(
|
||||
#[weak]
|
||||
label,
|
||||
move || {
|
||||
label.set_text(load_strings(None).fingerprint_prompt);
|
||||
label.remove_css_class("success");
|
||||
label.remove_css_class("failed");
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
// Extract the fp_listener into its own Rc<RefCell<>> for signal self-wiring
|
||||
let fp_rc = {
|
||||
let mut s = state.borrow_mut();
|
||||
let listener = std::mem::replace(&mut s.fp_listener, FingerprintListener::new());
|
||||
Rc::new(RefCell::new(listener))
|
||||
};
|
||||
|
||||
FingerprintListener::start(&fp_rc, &user.username, on_success, on_failure);
|
||||
|
||||
// Store back the Rc reference for stop() on unlock
|
||||
state.borrow_mut().fp_listener_rc = Some(fp_rc);
|
||||
}
|
||||
|
||||
// Fade-in on map
|
||||
window.connect_map(|w| {
|
||||
glib::idle_add_local_once(clone!(
|
||||
@ -326,91 +370,7 @@ pub fn create_lockscreen_window(
|
||||
}
|
||||
));
|
||||
|
||||
LockscreenHandles {
|
||||
window,
|
||||
fp_label,
|
||||
password_entry: password_entry.clone(),
|
||||
unlock_callback,
|
||||
username: user.username,
|
||||
state: state.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the fingerprint label and store the listener reference for stop-on-unlock.
|
||||
/// Does NOT start verification — call `start_fingerprint()` on one monitor for that.
|
||||
pub fn show_fingerprint_label(
|
||||
handles: &LockscreenHandles,
|
||||
fp_rc: &Rc<RefCell<FingerprintListener>>,
|
||||
) {
|
||||
let strings = load_strings(None);
|
||||
handles.fp_label.set_text(strings.fingerprint_prompt);
|
||||
handles.fp_label.set_visible(true);
|
||||
|
||||
// Store the Rc reference for stop() on unlock
|
||||
handles.state.borrow_mut().fp_listener_rc = Some(fp_rc.clone());
|
||||
}
|
||||
|
||||
/// Start fingerprint verification on a single monitor's handles.
|
||||
/// Wires up on_success/on_failure callbacks and calls start_async.
|
||||
pub fn start_fingerprint(
|
||||
handles: &LockscreenHandles,
|
||||
fp_rc: &Rc<RefCell<FingerprintListener>>,
|
||||
) {
|
||||
let fp_label_success = handles.fp_label.clone();
|
||||
let fp_label_fail = handles.fp_label.clone();
|
||||
let unlock_cb_fp = handles.unlock_callback.clone();
|
||||
|
||||
let on_success = move || {
|
||||
let label = fp_label_success.clone();
|
||||
let cb = unlock_cb_fp.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
label.set_text(load_strings(None).fingerprint_success);
|
||||
label.add_css_class("success");
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
let on_failure = move || {
|
||||
let label = fp_label_fail.clone();
|
||||
glib::idle_add_local_once(clone!(
|
||||
#[weak]
|
||||
label,
|
||||
move || {
|
||||
let strings = load_strings(None);
|
||||
label.set_text(strings.fingerprint_failed);
|
||||
label.add_css_class("failed");
|
||||
// Reset after 2 seconds
|
||||
glib::timeout_add_local_once(
|
||||
std::time::Duration::from_secs(2),
|
||||
clone!(
|
||||
#[weak]
|
||||
label,
|
||||
move || {
|
||||
label.set_text(load_strings(None).fingerprint_prompt);
|
||||
label.remove_css_class("success");
|
||||
label.remove_css_class("failed");
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
let fp_label_exhausted = handles.fp_label.clone();
|
||||
let on_exhausted = move || {
|
||||
let label = fp_label_exhausted.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
label.set_visible(false);
|
||||
});
|
||||
};
|
||||
|
||||
let username = handles.username.clone();
|
||||
let fp_rc_clone = fp_rc.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
FingerprintListener::start_async(
|
||||
&fp_rc_clone, &username, on_success, on_failure, on_exhausted,
|
||||
).await;
|
||||
});
|
||||
window
|
||||
}
|
||||
|
||||
/// Create a Picture widget for the wallpaper background.
|
||||
|
||||
60
src/main.rs
60
src/main.rs
@ -13,12 +13,9 @@ use gdk4 as gdk;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{self as gtk, gio};
|
||||
use gtk4_session_lock;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
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");
|
||||
@ -78,67 +75,27 @@ fn activate_with_session_lock(
|
||||
app_clone.quit();
|
||||
});
|
||||
|
||||
// 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(
|
||||
let window = lockscreen::create_lockscreen_window(
|
||||
bg_path,
|
||||
config,
|
||||
app,
|
||||
unlock_callback.clone(),
|
||||
);
|
||||
lock.assign_window_to_monitor(&handles.window, &monitor);
|
||||
handles.window.present();
|
||||
all_handles.push(handles);
|
||||
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)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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)]
|
||||
@ -152,19 +109,14 @@ fn activate_without_lock(
|
||||
app_clone.quit();
|
||||
});
|
||||
|
||||
let handles = lockscreen::create_lockscreen_window(
|
||||
let window = lockscreen::create_lockscreen_window(
|
||||
bg_path,
|
||||
config,
|
||||
app,
|
||||
unlock_callback,
|
||||
);
|
||||
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]);
|
||||
}
|
||||
window.set_default_size(800, 600);
|
||||
window.present();
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user