diff --git a/CLAUDE.md b/CLAUDE.md index 3b7af4e..d3c928e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,8 +44,9 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - `sessions.rs` — Wayland/X11 Sessions aus .desktop Files - `power.rs` — Reboot/Shutdown via loginctl - `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen -- `config.rs` — TOML-Config ([appearance] background, gtk-theme) + Wallpaper-Fallback -- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC, Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions) +- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback +- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback +- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions) - `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger - `resources/style.css` — Catppuccin-inspiriertes Theme @@ -57,6 +58,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - **Socket-Cancellation**: `Arc>>` + `AtomicBool` für saubere Abbrüche - **Avatar-Cache**: `HashMap` in `Rc>` - **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate +- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd. - **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur) - **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt - **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config diff --git a/Cargo.toml b/Cargo.toml index 7e85611..df0a11a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moongreet" -version = "0.5.3" +version = "0.6.0" edition = "2024" description = "A greetd greeter for Wayland with GTK4 and Layer Shell" license = "MIT" diff --git a/DECISIONS.md b/DECISIONS.md index 00ca23a..d2578c2 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,12 @@ # Decisions +## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM + +- **Who**: Ragnar, Dom +- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen. +- **Tradeoffs**: Direct fprintd D-Bus verification (like moonlock) can't start a greetd session — greetd controls session creation via PAM. Using greetd multi-stage means PAM decides the auth order (fingerprint first, then password fallback), not truly parallel. Acceptable — matches standard pam_fprintd behavior. +- **How**: Replace single-pass auth with a loop over auth_message rounds. Secret prompts get the password, non-secret prompts (fprintd) get None and block until PAM resolves. fprintd D-Bus probe (gio::DBusProxy) only for UI — detecting device availability and enrolled fingers. 60s socket timeout when fingerprint available. Config option `fingerprint-enabled` (default true). + ## 2026-03-28 – Remove embedded wallpaper from binary - **Who**: Selene, Dom diff --git a/README.md b/README.md index dc76e08..9e0bb12 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Part of the Moonarch ecosystem. - **Multi-monitor** — Greeter on primary, wallpaper on all monitors - **i18n** — German and English (auto-detected from system locale) - **Faillock warning** — Warns after 2 failed attempts, locked message after 3 +- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable) ## Requirements diff --git a/resources/style.css b/resources/style.css index f314212..7538428 100644 --- a/resources/style.css +++ b/resources/style.css @@ -54,6 +54,13 @@ window.wallpaper { font-size: 14px; } +/* Fingerprint prompt label */ +.fingerprint-label { + color: alpha(white, 0.6); + font-size: 13px; + margin-top: 8px; +} + /* User list on the bottom left */ .user-list { background-color: transparent; diff --git a/src/config.rs b/src/config.rs index 4bf6f18..6026081 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,14 +25,28 @@ struct Appearance { background_blur: Option, #[serde(rename = "gtk-theme")] gtk_theme: Option, + #[serde(rename = "fingerprint-enabled")] + fingerprint_enabled: Option, } /// Greeter configuration. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Config { pub background_path: Option, pub background_blur: Option, pub gtk_theme: Option, + pub fingerprint_enabled: bool, +} + +impl Default for Config { + fn default() -> Self { + Config { + background_path: None, + background_blur: None, + gtk_theme: None, + fingerprint_enabled: true, + } + } } /// Load config from TOML files. Later paths override earlier ones. @@ -64,6 +78,9 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { if appearance.gtk_theme.is_some() { merged.gtk_theme = appearance.gtk_theme; } + if let Some(fp) = appearance.fingerprint_enabled { + merged.fingerprint_enabled = fp; + } } } Err(e) => { @@ -77,7 +94,7 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { } } - log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme); + log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}, fingerprint={}", merged.background_path, merged.background_blur, merged.gtk_theme, merged.fingerprint_enabled); merged } @@ -120,6 +137,7 @@ mod tests { assert!(config.background_path.is_none()); assert!(config.background_blur.is_none()); assert!(config.gtk_theme.is_none()); + assert!(config.fingerprint_enabled); } #[test] @@ -248,4 +266,21 @@ mod tests { let result = resolve_background_path_with(&config, Path::new("/nonexistent")); assert!(result.is_none()); } + + #[test] + fn load_config_fingerprint_enabled_default_true() { + let paths = vec![PathBuf::from("/nonexistent/moongreet.toml")]; + let config = load_config(Some(&paths)); + assert!(config.fingerprint_enabled); + } + + #[test] + fn load_config_fingerprint_disabled() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moongreet.toml"); + fs::write(&conf, "[appearance]\nfingerprint-enabled = false\n").unwrap(); + let paths = vec![conf]; + let config = load_config(Some(&paths)); + assert!(!config.fingerprint_enabled); + } } diff --git a/src/fingerprint.rs b/src/fingerprint.rs new file mode 100644 index 0000000..74897b0 --- /dev/null +++ b/src/fingerprint.rs @@ -0,0 +1,137 @@ +// ABOUTME: fprintd D-Bus probe for fingerprint device availability. +// ABOUTME: Checks if fprintd is running and the user has enrolled fingerprints. + +use gio::prelude::*; +use gtk4::gio; + +const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint"; +const FPRINTD_MANAGER_PATH: &str = "/net/reactivated/Fprint/Manager"; +const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager"; +const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device"; + +const DBUS_TIMEOUT_MS: i32 = 3000; + +/// Lightweight fprintd probe — detects device availability and finger enrollment. +/// Does NOT perform verification (that happens through greetd/PAM). +pub struct FingerprintProbe { + device_proxy: Option, +} + +impl FingerprintProbe { + /// Create a probe without any D-Bus connections. + /// Call `init_async().await` to connect to fprintd. + pub fn new() -> Self { + FingerprintProbe { + device_proxy: None, + } + } + + /// Connect to fprintd on the system bus and discover the default device. + pub async fn init_async(&mut self) { + let manager = match gio::DBusProxy::for_bus_future( + gio::BusType::System, + gio::DBusProxyFlags::NONE, + None, + FPRINTD_BUS_NAME, + FPRINTD_MANAGER_PATH, + FPRINTD_MANAGER_IFACE, + ) + .await + { + Ok(m) => m, + Err(e) => { + log::debug!("fprintd manager not available: {e}"); + return; + } + }; + + let result = match manager + .call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS) + .await + { + Ok(r) => r, + Err(e) => { + log::debug!("fprintd GetDefaultDevice failed: {e}"); + return; + } + }; + + let device_path = match result.child_value(0).get::() { + Some(p) => p, + None => { + log::debug!("fprintd: unexpected GetDefaultDevice response type"); + return; + } + }; + if device_path.is_empty() { + return; + } + + match gio::DBusProxy::for_bus_future( + gio::BusType::System, + gio::DBusProxyFlags::NONE, + None, + FPRINTD_BUS_NAME, + &device_path, + FPRINTD_DEVICE_IFACE, + ) + .await + { + Ok(proxy) => { + self.device_proxy = Some(proxy); + } + Err(e) => { + log::debug!("fprintd device proxy failed: {e}"); + } + } + } + + /// Check if the user has enrolled fingerprints on the default device. + /// Returns false if fprintd is unavailable or the user has no enrollments. + pub async fn is_available_async(&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, + DBUS_TIMEOUT_MS, + ) + .await + { + Ok(result) => match result.child_value(0).get::>() { + Some(fingers) => !fingers.is_empty(), + None => { + log::debug!("fprintd: unexpected ListEnrolledFingers response type"); + false + } + }, + Err(_) => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_probe_has_no_device() { + let probe = FingerprintProbe::new(); + assert!(probe.device_proxy.is_none()); + } + + #[test] + fn constants_are_defined() { + assert!(!FPRINTD_BUS_NAME.is_empty()); + assert!(!FPRINTD_MANAGER_PATH.is_empty()); + assert!(!FPRINTD_MANAGER_IFACE.is_empty()); + assert!(!FPRINTD_DEVICE_IFACE.is_empty()); + assert!(DBUS_TIMEOUT_MS > 0); + } +} diff --git a/src/greeter.rs b/src/greeter.rs index 08287ce..3d10560 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -194,6 +194,7 @@ struct GreeterState { failed_attempts: HashMap, greetd_sock: Arc>>, login_cancelled: Arc, + fingerprint_available: bool, } /// Create the main greeter window with login UI. @@ -224,6 +225,7 @@ pub fn create_greeter_window( } let strings = load_strings(None); + let fingerprint_enabled = config.fingerprint_enabled; let all_users = users::get_users(None); let all_sessions = sessions::get_sessions(None, None); log::debug!("Greeter window: {} user(s), {} session(s)", all_users.len(), all_sessions.len()); @@ -238,6 +240,7 @@ pub fn create_greeter_window( failed_attempts: HashMap::new(), greetd_sock: Arc::new(Mutex::new(None)), login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)), + fingerprint_available: false, })); // Root overlay for layering @@ -308,6 +311,12 @@ pub fn create_greeter_window( error_label.set_visible(false); login_box.append(&error_label); + // Fingerprint label (hidden until probe confirms availability) + let fp_label = gtk::Label::new(None); + fp_label.add_css_class("fingerprint-label"); + fp_label.set_visible(false); + login_box.append(&fp_label); + login_box.set_halign(gtk::Align::Center); main_box.append(&login_box); @@ -348,6 +357,8 @@ pub fn create_greeter_window( #[weak] error_label, #[weak] + fp_label, + #[weak] session_dropdown, #[weak] window, @@ -364,9 +375,12 @@ pub fn create_greeter_window( &username_label, &password_entry, &error_label, + &fp_label, &session_dropdown, &sessions_rc, &window, + fingerprint_enabled, + strings, ); } )); @@ -497,6 +511,8 @@ pub fn create_greeter_window( #[weak] error_label, #[weak] + fp_label, + #[weak] session_dropdown, #[weak] window, @@ -514,6 +530,8 @@ pub fn create_greeter_window( #[weak] error_label, #[weak] + fp_label, + #[weak] session_dropdown, #[weak] window, @@ -525,9 +543,12 @@ pub fn create_greeter_window( &username_label, &password_entry, &error_label, + &fp_label, &session_dropdown, &sessions_rc, &window, + fingerprint_enabled, + strings, ); } )); @@ -545,9 +566,12 @@ fn select_initial_user( username_label: >k::Label, password_entry: >k::PasswordEntry, error_label: >k::Label, + fp_label: >k::Label, session_dropdown: >k::DropDown, sessions: &[Session], window: >k::ApplicationWindow, + fingerprint_enabled: bool, + strings: &'static Strings, ) { if users.is_empty() { return; @@ -567,9 +591,12 @@ fn select_initial_user( username_label, password_entry, error_label, + fp_label, session_dropdown, sessions, window, + fingerprint_enabled, + strings, ); } @@ -581,19 +608,24 @@ fn switch_to_user( username_label: >k::Label, password_entry: >k::PasswordEntry, error_label: >k::Label, + fp_label: >k::Label, session_dropdown: >k::DropDown, sessions: &[Session], window: >k::ApplicationWindow, + fingerprint_enabled: bool, + strings: &'static Strings, ) { log::debug!("Switching to user: {}", user.username); { let mut s = state.borrow_mut(); s.selected_user = Some(user.clone()); + s.fingerprint_available = false; } username_label.set_text(user.display_name()); password_entry.set_text(""); error_label.set_visible(false); + fp_label.set_visible(false); // Update avatar let cached = { @@ -618,6 +650,27 @@ fn switch_to_user( // Pre-select last used session for this user select_last_session(&user.username, session_dropdown, sessions); + // Probe fprintd for fingerprint availability + if fingerprint_enabled { + let username = user.username.clone(); + glib::spawn_future_local(clone!( + #[weak] + fp_label, + #[strong] + state, + async move { + let mut probe = crate::fingerprint::FingerprintProbe::new(); + probe.init_async().await; + let available = probe.is_available_async(&username).await; + state.borrow_mut().fingerprint_available = available; + fp_label.set_visible(available); + if available { + fp_label.set_text(strings.fingerprint_prompt); + } + } + )); + } + password_entry.grab_focus(); } @@ -885,6 +938,7 @@ fn attempt_login( let session_name = session.name.clone(); let greetd_sock = state.borrow().greetd_sock.clone(); let login_cancelled = state.borrow().login_cancelled.clone(); + let fingerprint_available = state.borrow().fingerprint_available; glib::spawn_future_local(clone!( #[weak] @@ -908,6 +962,7 @@ fn attempt_login( &greetd_sock, &login_cancelled, strings, + fingerprint_available, ) }) .await; @@ -925,6 +980,7 @@ fn attempt_login( let warning = faillock_warning(*count, strings); drop(s); + set_login_sensitive(&password_entry, &session_dropdown, true); show_greetd_error( &error_label, &password_entry, @@ -935,24 +991,23 @@ fn attempt_login( let current = error_label.text().to_string(); error_label.set_text(&format!("{current}\n{w}")); } - set_login_sensitive(&password_entry, &session_dropdown, true); } Ok(Ok(LoginResult::Error { message })) => { - show_error(&error_label, &password_entry, &message); set_login_sensitive(&password_entry, &session_dropdown, true); + show_error(&error_label, &password_entry, &message); } Ok(Ok(LoginResult::Cancelled)) => { set_login_sensitive(&password_entry, &session_dropdown, true); } Ok(Err(e)) => { log::error!("Login worker error: {e}"); - show_error(&error_label, &password_entry, strings.socket_error); set_login_sensitive(&password_entry, &session_dropdown, true); + show_error(&error_label, &password_entry, strings.socket_error); } Err(_) => { log::error!("Login worker panicked"); - show_error(&error_label, &password_entry, strings.socket_error); set_login_sensitive(&password_entry, &session_dropdown, true); + show_error(&error_label, &password_entry, strings.socket_error); } } } @@ -983,6 +1038,7 @@ fn login_worker( greetd_sock: &Arc>>, login_cancelled: &Arc, strings: &Strings, + fingerprint_available: bool, ) -> Result { if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) { log::debug!("Login cancelled before connect"); @@ -991,7 +1047,9 @@ fn login_worker( log::debug!("Connecting to greetd socket: {sock_path}"); let mut sock = UnixStream::connect(sock_path).map_err(|e| e.to_string())?; - if let Err(e) = sock.set_read_timeout(Some(std::time::Duration::from_secs(10))) { + // Longer timeout when fingerprint is available — pam_fprintd waits for scan + let read_timeout_secs = if fingerprint_available { 60 } else { 10 }; + if let Err(e) = sock.set_read_timeout(Some(std::time::Duration::from_secs(read_timeout_secs))) { log::warn!("Failed to set read timeout: {e}"); } if let Err(e) = sock.set_write_timeout(Some(std::time::Duration::from_secs(10))) { @@ -1023,11 +1081,40 @@ fn login_worker( } } - // Step 2: Send password if auth message received - if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") { - log::debug!("Sending auth response for {username}"); - response = - ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?; + // Step 2: Handle auth_message loop (supports multi-stage PAM, e.g. fprintd + password) + const MAX_AUTH_ROUNDS: u32 = 5; + let mut auth_round = 0; + + while response.get("type").and_then(|v| v.as_str()) == Some("auth_message") { + auth_round += 1; + if auth_round > MAX_AUTH_ROUNDS { + log::warn!("Too many auth rounds ({auth_round}), aborting"); + let _ = ipc::cancel_session(&mut sock); + return Ok(LoginResult::Error { + message: strings.auth_failed.to_string(), + }); + } + + if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) { + return Ok(LoginResult::Cancelled); + } + + let msg_type = response + .get("auth_message_type") + .and_then(|v| v.as_str()) + .unwrap_or("secret"); + + if msg_type == "secret" { + log::debug!("Sending password for {username} (round {auth_round})"); + response = + ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?; + } else { + // Non-secret prompt (e.g. fprintd "Place finger on reader") + // PAM handles the actual verification; this blocks until resolved + log::debug!("Acknowledging non-secret auth prompt (round {auth_round})"); + response = + ipc::post_auth_response(&mut sock, None).map_err(|e| e.to_string())?; + } if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) { return Ok(LoginResult::Cancelled); @@ -1040,14 +1127,6 @@ fn login_worker( username: username.to_string(), }); } - - if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") { - // Multi-stage auth is not supported - let _ = ipc::cancel_session(&mut sock); - return Ok(LoginResult::Error { - message: strings.multi_stage_unsupported.to_string(), - }); - } } // Step 3: Start session @@ -1475,7 +1554,7 @@ mod tests { let result = login_worker( "alice", "wrongpass", "/usr/bin/niri", &sock_path, &default_greetd_sock(), &default_cancelled(), - load_strings(Some("en")), + load_strings(Some("en")), false, ); let result = result.unwrap(); @@ -1517,7 +1596,7 @@ mod tests { let result = login_worker( "alice", "correct", "/usr/bin/bash", &sock_path, &default_greetd_sock(), &default_cancelled(), - load_strings(Some("en")), + load_strings(Some("en")), false, ); let result = result.unwrap(); @@ -1526,40 +1605,104 @@ mod tests { } #[test] - fn login_worker_multi_stage_rejected() { + fn login_worker_multi_stage_fingerprint_then_password() { let (sock_path, handle) = fake_greetd(|stream| { // create_session let _msg = ipc::recv_message(stream).unwrap(); + ipc::send_message(stream, &serde_json::json!({ + "type": "auth_message", + "auth_message_type": "visible", + "auth_message": "Place your finger on the reader", + })).unwrap(); + + // post_auth_response with None (fingerprint prompt acknowledged) + let msg = ipc::recv_message(stream).unwrap(); + assert!(msg["response"].is_null()); + + // Fingerprint failed, PAM falls through to password ipc::send_message(stream, &serde_json::json!({ "type": "auth_message", "auth_message_type": "secret", "auth_message": "Password: ", })).unwrap(); - // post_auth_response → another auth_message (TOTP) - let _msg = ipc::recv_message(stream).unwrap(); - ipc::send_message(stream, &serde_json::json!({ - "type": "auth_message", - "auth_message_type": "visible", - "auth_message": "TOTP: ", - })).unwrap(); + // post_auth_response with password + let msg = ipc::recv_message(stream).unwrap(); + assert_eq!(msg["response"], "correctpass"); + ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap(); - // cancel_session + // start_session let _msg = ipc::recv_message(stream).unwrap(); ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap(); }); let result = login_worker( - "alice", "pass", "/usr/bin/niri", + "alice", "correctpass", "/usr/bin/bash", &sock_path, &default_greetd_sock(), &default_cancelled(), - load_strings(Some("en")), + load_strings(Some("en")), true, + ); + + let result = result.unwrap(); + assert!(matches!(result, LoginResult::Success { .. })); + handle.join().unwrap(); + } + + #[test] + fn login_worker_multi_stage_fingerprint_success() { + let (sock_path, handle) = fake_greetd(|stream| { + // create_session + let _msg = ipc::recv_message(stream).unwrap(); + ipc::send_message(stream, &serde_json::json!({ + "type": "auth_message", + "auth_message_type": "visible", + "auth_message": "Place your finger on the reader", + })).unwrap(); + + // post_auth_response with None → fingerprint matched via PAM + let _msg = ipc::recv_message(stream).unwrap(); + ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap(); + + // start_session + let _msg = ipc::recv_message(stream).unwrap(); + ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap(); + }); + + let result = login_worker( + "alice", "", "/usr/bin/bash", + &sock_path, &default_greetd_sock(), &default_cancelled(), + load_strings(Some("en")), true, + ); + + let result = result.unwrap(); + assert!(matches!(result, LoginResult::Success { .. })); + handle.join().unwrap(); + } + + #[test] + fn login_worker_max_auth_rounds_exceeded() { + let (sock_path, handle) = fake_greetd(|stream| { + // create_session + let _msg = ipc::recv_message(stream).unwrap(); + + // Send 6 auth_messages (exceeds MAX_AUTH_ROUNDS=5) + for _ in 0..6 { + ipc::send_message(stream, &serde_json::json!({ + "type": "auth_message", + "auth_message_type": "visible", + "auth_message": "Prompt", + })).unwrap(); + let _msg = ipc::recv_message(stream).unwrap(); + } + }); + + let result = login_worker( + "alice", "pass", "/usr/bin/bash", + &sock_path, &default_greetd_sock(), &default_cancelled(), + load_strings(Some("en")), false, ); let result = result.unwrap(); assert!(matches!(result, LoginResult::Error { .. })); - if let LoginResult::Error { message } = result { - assert!(message.contains("Multi-stage")); - } handle.join().unwrap(); } @@ -1589,7 +1732,7 @@ mod tests { let result = login_worker( "alice", "pass", "/usr/bin/bash", &sock_path, &default_greetd_sock(), &default_cancelled(), - load_strings(Some("en")), + load_strings(Some("en")), false, ); let result = result.unwrap(); @@ -1604,7 +1747,7 @@ mod tests { let result = login_worker( "alice", "pass", "/usr/bin/niri", "/nonexistent/sock", &default_greetd_sock(), &cancelled, - load_strings(Some("en")), + load_strings(Some("en")), false, ); let result = result.unwrap(); @@ -1617,7 +1760,7 @@ mod tests { let result = login_worker( "alice", "pass", "/usr/bin/niri", "/nonexistent/sock", &default_greetd_sock(), &cancelled, - load_strings(Some("en")), + load_strings(Some("en")), false, ); assert!(result.is_err()); @@ -1647,7 +1790,7 @@ mod tests { let result = login_worker( "alice", "pass", "../../../etc/evil", &sock_path, &default_greetd_sock(), &default_cancelled(), - load_strings(Some("en")), + load_strings(Some("en")), false, ); let result = result.unwrap(); @@ -1679,7 +1822,7 @@ mod tests { let result = login_worker( "alice", "pass", "niri-session", &sock_path, &default_greetd_sock(), &default_cancelled(), - load_strings(Some("en")), + load_strings(Some("en")), false, ); let result = result.unwrap(); diff --git a/src/i18n.rs b/src/i18n.rs index 2d36c7e..8a9c276 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -23,7 +23,7 @@ pub struct Strings { pub greetd_sock_unreachable: &'static str, pub auth_failed: &'static str, pub wrong_password: &'static str, - pub multi_stage_unsupported: &'static str, + pub fingerprint_prompt: &'static str, pub invalid_session_command: &'static str, pub session_start_failed: &'static str, pub reboot_failed: &'static str, @@ -47,7 +47,7 @@ const STRINGS_DE: Strings = Strings { greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar", auth_failed: "Authentifizierung fehlgeschlagen", wrong_password: "Falsches Passwort", - multi_stage_unsupported: "Mehrstufige Authentifizierung wird nicht unterstützt", + fingerprint_prompt: "Fingerabdruck auflegen oder Passwort eingeben", invalid_session_command: "Ungültiger Session-Befehl", session_start_failed: "Session konnte nicht gestartet werden", reboot_failed: "Neustart fehlgeschlagen", @@ -69,7 +69,7 @@ const STRINGS_EN: Strings = Strings { greetd_sock_unreachable: "GREETD_SOCK unreachable", auth_failed: "Authentication failed", wrong_password: "Wrong password", - multi_stage_unsupported: "Multi-stage authentication is not supported", + fingerprint_prompt: "Place finger on reader or enter password", invalid_session_command: "Invalid session command", session_start_failed: "Failed to start session", reboot_failed: "Reboot failed", @@ -282,6 +282,7 @@ mod tests { assert!(!s.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set"); assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed"); assert!(!s.wrong_password.is_empty(), "{locale}: wrong_password"); + assert!(!s.fingerprint_prompt.is_empty(), "{locale}: fingerprint_prompt"); assert!(!s.reboot_failed.is_empty(), "{locale}: reboot_failed"); assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed"); assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining"); diff --git a/src/main.rs b/src/main.rs index dc44f60..911ade3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ // ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows. mod config; +mod fingerprint; mod greeter; mod i18n; mod ipc;