feat: add fprintd fingerprint authentication via greetd multi-stage PAM (v0.6.0)
Fingerprint auth was missing because moongreet rejected multi-stage auth_message sequences from greetd. With pam_fprintd.so in the PAM stack, greetd sends non-secret prompts for fingerprint and secret prompts for password — moongreet now handles both in a loop. - Replace single-pass auth with multi-stage auth_message loop - fprintd D-Bus probe (gio::DBusProxy) for UI feedback only - Fingerprint label shown when device available and fingers enrolled - 60s socket timeout when fingerprint available (pam_fprintd scan time) - Config option: [appearance] fingerprint-enabled (default: true) - Fix: password entry focus loss after auth error (grab_focus while widget was insensitive — now re-enable before grab_focus)
This commit is contained in:
parent
77b94a560d
commit
a462b2cf06
@ -44,8 +44,9 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
|||||||
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
|
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
|
||||||
- `power.rs` — Reboot/Shutdown via loginctl
|
- `power.rs` — Reboot/Shutdown via loginctl
|
||||||
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
|
- `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
|
- `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
|
||||||
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC, Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions)
|
- `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
|
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger
|
||||||
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
- `resources/style.css` — Catppuccin-inspiriertes Theme
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
|
|||||||
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
|
||||||
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
|
||||||
- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate
|
- **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)
|
- **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
|
- **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
|
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moongreet"
|
name = "moongreet"
|
||||||
version = "0.5.3"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
# Decisions
|
# 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
|
## 2026-03-28 – Remove embedded wallpaper from binary
|
||||||
|
|
||||||
- **Who**: Selene, Dom
|
- **Who**: Selene, Dom
|
||||||
|
|||||||
@ -15,6 +15,7 @@ Part of the Moonarch ecosystem.
|
|||||||
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors
|
||||||
- **i18n** — German and English (auto-detected from system locale)
|
- **i18n** — German and English (auto-detected from system locale)
|
||||||
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3
|
||||||
|
- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,13 @@ window.wallpaper {
|
|||||||
font-size: 14px;
|
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 on the bottom left */
|
||||||
.user-list {
|
.user-list {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|||||||
@ -25,14 +25,28 @@ struct Appearance {
|
|||||||
background_blur: Option<f32>,
|
background_blur: Option<f32>,
|
||||||
#[serde(rename = "gtk-theme")]
|
#[serde(rename = "gtk-theme")]
|
||||||
gtk_theme: Option<String>,
|
gtk_theme: Option<String>,
|
||||||
|
#[serde(rename = "fingerprint-enabled")]
|
||||||
|
fingerprint_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Greeter configuration.
|
/// Greeter configuration.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub background_path: Option<String>,
|
pub background_path: Option<String>,
|
||||||
pub background_blur: Option<f32>,
|
pub background_blur: Option<f32>,
|
||||||
pub gtk_theme: Option<String>,
|
pub gtk_theme: Option<String>,
|
||||||
|
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.
|
/// 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() {
|
if appearance.gtk_theme.is_some() {
|
||||||
merged.gtk_theme = appearance.gtk_theme;
|
merged.gtk_theme = appearance.gtk_theme;
|
||||||
}
|
}
|
||||||
|
if let Some(fp) = appearance.fingerprint_enabled {
|
||||||
|
merged.fingerprint_enabled = fp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +137,7 @@ mod tests {
|
|||||||
assert!(config.background_path.is_none());
|
assert!(config.background_path.is_none());
|
||||||
assert!(config.background_blur.is_none());
|
assert!(config.background_blur.is_none());
|
||||||
assert!(config.gtk_theme.is_none());
|
assert!(config.gtk_theme.is_none());
|
||||||
|
assert!(config.fingerprint_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -248,4 +266,21 @@ mod tests {
|
|||||||
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
|
||||||
assert!(result.is_none());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
src/fingerprint.rs
Normal file
137
src/fingerprint.rs
Normal file
@ -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<gio::DBusProxy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<String>() {
|
||||||
|
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::<Vec<String>>() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
217
src/greeter.rs
217
src/greeter.rs
@ -194,6 +194,7 @@ struct GreeterState {
|
|||||||
failed_attempts: HashMap<String, u32>,
|
failed_attempts: HashMap<String, u32>,
|
||||||
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
greetd_sock: Arc<Mutex<Option<UnixStream>>>,
|
||||||
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
login_cancelled: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
fingerprint_available: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the main greeter window with login UI.
|
/// Create the main greeter window with login UI.
|
||||||
@ -224,6 +225,7 @@ pub fn create_greeter_window(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let strings = load_strings(None);
|
let strings = load_strings(None);
|
||||||
|
let fingerprint_enabled = config.fingerprint_enabled;
|
||||||
let all_users = users::get_users(None);
|
let all_users = users::get_users(None);
|
||||||
let all_sessions = sessions::get_sessions(None, None);
|
let all_sessions = sessions::get_sessions(None, None);
|
||||||
log::debug!("Greeter window: {} user(s), {} session(s)", all_users.len(), all_sessions.len());
|
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(),
|
failed_attempts: HashMap::new(),
|
||||||
greetd_sock: Arc::new(Mutex::new(None)),
|
greetd_sock: Arc::new(Mutex::new(None)),
|
||||||
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
|
fingerprint_available: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Root overlay for layering
|
// Root overlay for layering
|
||||||
@ -308,6 +311,12 @@ pub fn create_greeter_window(
|
|||||||
error_label.set_visible(false);
|
error_label.set_visible(false);
|
||||||
login_box.append(&error_label);
|
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);
|
login_box.set_halign(gtk::Align::Center);
|
||||||
main_box.append(&login_box);
|
main_box.append(&login_box);
|
||||||
|
|
||||||
@ -348,6 +357,8 @@ pub fn create_greeter_window(
|
|||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
#[weak]
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[weak]
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
@ -364,9 +375,12 @@ pub fn create_greeter_window(
|
|||||||
&username_label,
|
&username_label,
|
||||||
&password_entry,
|
&password_entry,
|
||||||
&error_label,
|
&error_label,
|
||||||
|
&fp_label,
|
||||||
&session_dropdown,
|
&session_dropdown,
|
||||||
&sessions_rc,
|
&sessions_rc,
|
||||||
&window,
|
&window,
|
||||||
|
fingerprint_enabled,
|
||||||
|
strings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@ -497,6 +511,8 @@ pub fn create_greeter_window(
|
|||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
#[weak]
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[weak]
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
@ -514,6 +530,8 @@ pub fn create_greeter_window(
|
|||||||
#[weak]
|
#[weak]
|
||||||
error_label,
|
error_label,
|
||||||
#[weak]
|
#[weak]
|
||||||
|
fp_label,
|
||||||
|
#[weak]
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
#[weak]
|
#[weak]
|
||||||
window,
|
window,
|
||||||
@ -525,9 +543,12 @@ pub fn create_greeter_window(
|
|||||||
&username_label,
|
&username_label,
|
||||||
&password_entry,
|
&password_entry,
|
||||||
&error_label,
|
&error_label,
|
||||||
|
&fp_label,
|
||||||
&session_dropdown,
|
&session_dropdown,
|
||||||
&sessions_rc,
|
&sessions_rc,
|
||||||
&window,
|
&window,
|
||||||
|
fingerprint_enabled,
|
||||||
|
strings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@ -545,9 +566,12 @@ fn select_initial_user(
|
|||||||
username_label: >k::Label,
|
username_label: >k::Label,
|
||||||
password_entry: >k::PasswordEntry,
|
password_entry: >k::PasswordEntry,
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
|
fp_label: >k::Label,
|
||||||
session_dropdown: >k::DropDown,
|
session_dropdown: >k::DropDown,
|
||||||
sessions: &[Session],
|
sessions: &[Session],
|
||||||
window: >k::ApplicationWindow,
|
window: >k::ApplicationWindow,
|
||||||
|
fingerprint_enabled: bool,
|
||||||
|
strings: &'static Strings,
|
||||||
) {
|
) {
|
||||||
if users.is_empty() {
|
if users.is_empty() {
|
||||||
return;
|
return;
|
||||||
@ -567,9 +591,12 @@ fn select_initial_user(
|
|||||||
username_label,
|
username_label,
|
||||||
password_entry,
|
password_entry,
|
||||||
error_label,
|
error_label,
|
||||||
|
fp_label,
|
||||||
session_dropdown,
|
session_dropdown,
|
||||||
sessions,
|
sessions,
|
||||||
window,
|
window,
|
||||||
|
fingerprint_enabled,
|
||||||
|
strings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,19 +608,24 @@ fn switch_to_user(
|
|||||||
username_label: >k::Label,
|
username_label: >k::Label,
|
||||||
password_entry: >k::PasswordEntry,
|
password_entry: >k::PasswordEntry,
|
||||||
error_label: >k::Label,
|
error_label: >k::Label,
|
||||||
|
fp_label: >k::Label,
|
||||||
session_dropdown: >k::DropDown,
|
session_dropdown: >k::DropDown,
|
||||||
sessions: &[Session],
|
sessions: &[Session],
|
||||||
window: >k::ApplicationWindow,
|
window: >k::ApplicationWindow,
|
||||||
|
fingerprint_enabled: bool,
|
||||||
|
strings: &'static Strings,
|
||||||
) {
|
) {
|
||||||
log::debug!("Switching to user: {}", user.username);
|
log::debug!("Switching to user: {}", user.username);
|
||||||
{
|
{
|
||||||
let mut s = state.borrow_mut();
|
let mut s = state.borrow_mut();
|
||||||
s.selected_user = Some(user.clone());
|
s.selected_user = Some(user.clone());
|
||||||
|
s.fingerprint_available = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
username_label.set_text(user.display_name());
|
username_label.set_text(user.display_name());
|
||||||
password_entry.set_text("");
|
password_entry.set_text("");
|
||||||
error_label.set_visible(false);
|
error_label.set_visible(false);
|
||||||
|
fp_label.set_visible(false);
|
||||||
|
|
||||||
// Update avatar
|
// Update avatar
|
||||||
let cached = {
|
let cached = {
|
||||||
@ -618,6 +650,27 @@ fn switch_to_user(
|
|||||||
// Pre-select last used session for this user
|
// Pre-select last used session for this user
|
||||||
select_last_session(&user.username, session_dropdown, sessions);
|
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();
|
password_entry.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -885,6 +938,7 @@ fn attempt_login(
|
|||||||
let session_name = session.name.clone();
|
let session_name = session.name.clone();
|
||||||
let greetd_sock = state.borrow().greetd_sock.clone();
|
let greetd_sock = state.borrow().greetd_sock.clone();
|
||||||
let login_cancelled = state.borrow().login_cancelled.clone();
|
let login_cancelled = state.borrow().login_cancelled.clone();
|
||||||
|
let fingerprint_available = state.borrow().fingerprint_available;
|
||||||
|
|
||||||
glib::spawn_future_local(clone!(
|
glib::spawn_future_local(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
@ -908,6 +962,7 @@ fn attempt_login(
|
|||||||
&greetd_sock,
|
&greetd_sock,
|
||||||
&login_cancelled,
|
&login_cancelled,
|
||||||
strings,
|
strings,
|
||||||
|
fingerprint_available,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@ -925,6 +980,7 @@ fn attempt_login(
|
|||||||
let warning = faillock_warning(*count, strings);
|
let warning = faillock_warning(*count, strings);
|
||||||
drop(s);
|
drop(s);
|
||||||
|
|
||||||
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
show_greetd_error(
|
show_greetd_error(
|
||||||
&error_label,
|
&error_label,
|
||||||
&password_entry,
|
&password_entry,
|
||||||
@ -935,24 +991,23 @@ fn attempt_login(
|
|||||||
let current = error_label.text().to_string();
|
let current = error_label.text().to_string();
|
||||||
error_label.set_text(&format!("{current}\n{w}"));
|
error_label.set_text(&format!("{current}\n{w}"));
|
||||||
}
|
}
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
|
||||||
}
|
}
|
||||||
Ok(Ok(LoginResult::Error { message })) => {
|
Ok(Ok(LoginResult::Error { message })) => {
|
||||||
show_error(&error_label, &password_entry, &message);
|
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
|
show_error(&error_label, &password_entry, &message);
|
||||||
}
|
}
|
||||||
Ok(Ok(LoginResult::Cancelled)) => {
|
Ok(Ok(LoginResult::Cancelled)) => {
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
log::error!("Login worker error: {e}");
|
log::error!("Login worker error: {e}");
|
||||||
show_error(&error_label, &password_entry, strings.socket_error);
|
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
set_login_sensitive(&password_entry, &session_dropdown, true);
|
||||||
|
show_error(&error_label, &password_entry, strings.socket_error);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::error!("Login worker panicked");
|
log::error!("Login worker panicked");
|
||||||
show_error(&error_label, &password_entry, strings.socket_error);
|
|
||||||
set_login_sensitive(&password_entry, &session_dropdown, true);
|
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<Mutex<Option<UnixStream>>>,
|
greetd_sock: &Arc<Mutex<Option<UnixStream>>>,
|
||||||
login_cancelled: &Arc<std::sync::atomic::AtomicBool>,
|
login_cancelled: &Arc<std::sync::atomic::AtomicBool>,
|
||||||
strings: &Strings,
|
strings: &Strings,
|
||||||
|
fingerprint_available: bool,
|
||||||
) -> Result<LoginResult, String> {
|
) -> Result<LoginResult, String> {
|
||||||
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
log::debug!("Login cancelled before connect");
|
log::debug!("Login cancelled before connect");
|
||||||
@ -991,7 +1047,9 @@ fn login_worker(
|
|||||||
|
|
||||||
log::debug!("Connecting to greetd socket: {sock_path}");
|
log::debug!("Connecting to greetd socket: {sock_path}");
|
||||||
let mut sock = UnixStream::connect(sock_path).map_err(|e| e.to_string())?;
|
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}");
|
log::warn!("Failed to set read timeout: {e}");
|
||||||
}
|
}
|
||||||
if let Err(e) = sock.set_write_timeout(Some(std::time::Duration::from_secs(10))) {
|
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
|
// Step 2: Handle auth_message loop (supports multi-stage PAM, e.g. fprintd + password)
|
||||||
if response.get("type").and_then(|v| v.as_str()) == Some("auth_message") {
|
const MAX_AUTH_ROUNDS: u32 = 5;
|
||||||
log::debug!("Sending auth response for {username}");
|
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 =
|
response =
|
||||||
ipc::post_auth_response(&mut sock, Some(password)).map_err(|e| e.to_string())?;
|
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) {
|
if login_cancelled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
return Ok(LoginResult::Cancelled);
|
return Ok(LoginResult::Cancelled);
|
||||||
@ -1040,14 +1127,6 @@ fn login_worker(
|
|||||||
username: username.to_string(),
|
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
|
// Step 3: Start session
|
||||||
@ -1475,7 +1554,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "wrongpass", "/usr/bin/niri",
|
"alice", "wrongpass", "/usr/bin/niri",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@ -1517,7 +1596,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "correct", "/usr/bin/bash",
|
"alice", "correct", "/usr/bin/bash",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@ -1526,40 +1605,104 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn login_worker_multi_stage_rejected() {
|
fn login_worker_multi_stage_fingerprint_then_password() {
|
||||||
let (sock_path, handle) = fake_greetd(|stream| {
|
let (sock_path, handle) = fake_greetd(|stream| {
|
||||||
// create_session
|
// create_session
|
||||||
let _msg = ipc::recv_message(stream).unwrap();
|
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!({
|
ipc::send_message(stream, &serde_json::json!({
|
||||||
"type": "auth_message",
|
"type": "auth_message",
|
||||||
"auth_message_type": "secret",
|
"auth_message_type": "secret",
|
||||||
"auth_message": "Password: ",
|
"auth_message": "Password: ",
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
|
|
||||||
// post_auth_response → another auth_message (TOTP)
|
// post_auth_response with password
|
||||||
let _msg = ipc::recv_message(stream).unwrap();
|
let msg = ipc::recv_message(stream).unwrap();
|
||||||
ipc::send_message(stream, &serde_json::json!({
|
assert_eq!(msg["response"], "correctpass");
|
||||||
"type": "auth_message",
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
"auth_message_type": "visible",
|
|
||||||
"auth_message": "TOTP: ",
|
|
||||||
})).unwrap();
|
|
||||||
|
|
||||||
// cancel_session
|
// start_session
|
||||||
let _msg = ipc::recv_message(stream).unwrap();
|
let _msg = ipc::recv_message(stream).unwrap();
|
||||||
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
ipc::send_message(stream, &serde_json::json!({"type": "success"})).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/niri",
|
"alice", "correctpass", "/usr/bin/bash",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&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();
|
let result = result.unwrap();
|
||||||
assert!(matches!(result, LoginResult::Error { .. }));
|
assert!(matches!(result, LoginResult::Error { .. }));
|
||||||
if let LoginResult::Error { message } = result {
|
|
||||||
assert!(message.contains("Multi-stage"));
|
|
||||||
}
|
|
||||||
handle.join().unwrap();
|
handle.join().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1589,7 +1732,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/bash",
|
"alice", "pass", "/usr/bin/bash",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@ -1604,7 +1747,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/niri",
|
"alice", "pass", "/usr/bin/niri",
|
||||||
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@ -1617,7 +1760,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "/usr/bin/niri",
|
"alice", "pass", "/usr/bin/niri",
|
||||||
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
"/nonexistent/sock", &default_greetd_sock(), &cancelled,
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
@ -1647,7 +1790,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "../../../etc/evil",
|
"alice", "pass", "../../../etc/evil",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
@ -1679,7 +1822,7 @@ mod tests {
|
|||||||
let result = login_worker(
|
let result = login_worker(
|
||||||
"alice", "pass", "niri-session",
|
"alice", "pass", "niri-session",
|
||||||
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
&sock_path, &default_greetd_sock(), &default_cancelled(),
|
||||||
load_strings(Some("en")),
|
load_strings(Some("en")), false,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = result.unwrap();
|
let result = result.unwrap();
|
||||||
|
|||||||
@ -23,7 +23,7 @@ pub struct Strings {
|
|||||||
pub greetd_sock_unreachable: &'static str,
|
pub greetd_sock_unreachable: &'static str,
|
||||||
pub auth_failed: &'static str,
|
pub auth_failed: &'static str,
|
||||||
pub wrong_password: &'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 invalid_session_command: &'static str,
|
||||||
pub session_start_failed: &'static str,
|
pub session_start_failed: &'static str,
|
||||||
pub reboot_failed: &'static str,
|
pub reboot_failed: &'static str,
|
||||||
@ -47,7 +47,7 @@ const STRINGS_DE: Strings = Strings {
|
|||||||
greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar",
|
greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar",
|
||||||
auth_failed: "Authentifizierung fehlgeschlagen",
|
auth_failed: "Authentifizierung fehlgeschlagen",
|
||||||
wrong_password: "Falsches Passwort",
|
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",
|
invalid_session_command: "Ungültiger Session-Befehl",
|
||||||
session_start_failed: "Session konnte nicht gestartet werden",
|
session_start_failed: "Session konnte nicht gestartet werden",
|
||||||
reboot_failed: "Neustart fehlgeschlagen",
|
reboot_failed: "Neustart fehlgeschlagen",
|
||||||
@ -69,7 +69,7 @@ const STRINGS_EN: Strings = Strings {
|
|||||||
greetd_sock_unreachable: "GREETD_SOCK unreachable",
|
greetd_sock_unreachable: "GREETD_SOCK unreachable",
|
||||||
auth_failed: "Authentication failed",
|
auth_failed: "Authentication failed",
|
||||||
wrong_password: "Wrong password",
|
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",
|
invalid_session_command: "Invalid session command",
|
||||||
session_start_failed: "Failed to start session",
|
session_start_failed: "Failed to start session",
|
||||||
reboot_failed: "Reboot failed",
|
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.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set");
|
||||||
assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed");
|
assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed");
|
||||||
assert!(!s.wrong_password.is_empty(), "{locale}: wrong_password");
|
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.reboot_failed.is_empty(), "{locale}: reboot_failed");
|
||||||
assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed");
|
assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed");
|
||||||
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows.
|
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows.
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod fingerprint;
|
||||||
mod greeter;
|
mod greeter;
|
||||||
mod i18n;
|
mod i18n;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user