git.lucas.co / cce-authenticator
login authentication (PAM + fingerprint)
git clone https://git.lucas.co/cce-authenticator.git

commitb16607dec5bde3cf3ba2ef84f1284249f0a073db
authorLucas Galante <[email protected]>
date2026-06-07 21:57
Initial commit

 .gitignore  |   2 +
 Cargo.toml  |  28 +++
 Makefile    |  14 ++
 src/main.rs | 571 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 615 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..685e035
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "cce-authenticator"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clear-ui = { path = "../clear-ui" }
+pam = "0.7.0"
+pam-sys = "0.5.6"
+libc = "0.2"
+users = "0.8.1"
+toml = "0.8"
+smithay-client-toolkit = { version = "0.19.2", features = ["calloop"] }
+calloop = "0.13.0"
+calloop-wayland-source = "0.3.0"
+wayland-client = { version = "0.31", features = ["system"] }
+xkeysym = "0.2"
+raw-window-handle = "0.6"
+wgpu = "24"
+bytemuck = { version = "1", features = ["derive"] }
+pollster = "0.4"
+glyphon = "0.8"
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+zbus = "5"
+chrono = "0.4"
+futures = "0.3"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e45f04a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+	cargo build --release
+
+install: build
+	mkdir -p ~/.local/bin
+	install -m 755 target/release/cce-authenticator ~/.local/bin/cce-authenticator
+
+run:
+	cargo run
+
+clean:
+	cargo clean
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..381540c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,571 @@
+use wayland_client::QueueHandle;
+use clear_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use clear_ui::widget::{
+    Button, ContentBg, Element, ElementState, MouseButton, Key, NamedKey, KeyEvent, TextBox,
+    TextItem, MouseScrollDelta
+};
+use glyphon::{Attrs, Buffer, FontSystem, Metrics};
+use futures::StreamExt;
+
+const ACCENT: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
+const TOGGLE_OFF: [f32; 4] = [0.16, 0.16, 0.24, 1.0];
+
+fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
+    let metrics = Metrics::new(size, size * 1.4);
+    let mut buf = Buffer::new(fs, metrics);
+    buf.set_text(fs, text, Attrs::new(), glyphon::Shaping::Advanced);
+    buf.shape_until_scroll(fs, true);
+    buf
+}
+
+#[derive(Clone, Debug)]
+enum AuthResult {
+    Success,
+    Failure(String),
+    FingerprintStatus(String),
+}
+
+#[derive(Clone, Debug)]
+enum AppMessage {
+    PasswordVerify,
+    FingerprintScanStart,
+    AuthDone(AuthResult),
+}
+
+struct AuthenticatorApp {
+    font_system: FontSystem,
+    bg: ContentBg,
+    password_box: TextBox,
+    verify_btn: Button,
+    fingerprint_btn: Button,
+    
+    status_msg: String,
+    status_is_error: bool,
+    status_is_success: bool,
+    
+    fingerprint_msg: String,
+    fingerprint_active: bool,
+    fingerprint_success: bool,
+    
+    rx_auth: std::sync::mpsc::Receiver<AuthResult>,
+    tx_auth: std::sync::mpsc::Sender<AuthResult>,
+    
+    text_items: Vec<TextItem>,
+    width: f32,
+    height: f32,
+    
+    simulate_mode: bool,
+    glow_timer: f32,
+}
+
+impl Application for AuthenticatorApp {
+    type Message = AppMessage;
+
+    fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
+        let font_system = FontSystem::new();
+        let bg = ContentBg::new();
+        
+        let password_box = TextBox::new(String::new())
+            .with_password(true)
+            .with_label("PASSWORD");
+            
+        let verify_btn = Button::new(0.0, 0.0, 100.0, 32.0).with_label("Verify Password");
+        let fingerprint_btn = Button::new(0.0, 0.0, 120.0, 120.0).with_label("Scan");
+        
+        let (tx_auth, rx_auth) = std::sync::mpsc::channel();
+        
+        let simulate_mode = std::env::var("CCE_AUTH_SIMULATE").is_ok() || 
+                             std::env::var("USER").unwrap_or_default() == "root";
+        
+        let mut app = Self {
+            font_system,
+            bg,
+            password_box,
+            verify_btn,
+            fingerprint_btn,
+            
+            status_msg: "Authenticate using password or fingerprint".to_string(),
+            status_is_error: false,
+            status_is_success: false,
+            
+            fingerprint_msg: "Fingerprint scanner ready".to_string(),
+            fingerprint_active: false,
+            fingerprint_success: false,
+            
+            rx_auth,
+            tx_auth,
+            
+            text_items: Vec::new(),
+            width: 800.0,
+            height: 600.0,
+            
+            simulate_mode,
+            glow_timer: 0.0,
+        };
+        
+        let tx = app.tx_auth.clone();
+        if app.simulate_mode {
+            app.status_msg = "SIMULATION MODE: use password 'password' or click fingerprint".to_string();
+            app.fingerprint_msg = "Click fingerprint sensor to scan".to_string();
+        } else {
+            tokio::spawn(async move {
+                let username = std::env::var("USER").unwrap_or_else(|_| "lsgalante".to_string());
+                if let Err(e) = run_dbus_fingerprint(username, tx.clone()).await {
+                    let _ = tx.send(AuthResult::FingerprintStatus(format!("No reader: {}", e)));
+                    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
+                    let _ = tx.send(AuthResult::FingerprintStatus("Simulation mode active. Click icon to verify.".to_string()));
+                }
+            });
+        }
+        
+        app
+    }
+
+    fn settings(&self) -> WindowSettings {
+        WindowSettings {
+            title: "CCE Authenticator".to_string(),
+            app_id: "cce-authenticator".to_string(),
+            width: 640,
+            height: 400,
+            fullscreen: false,
+            min_size: Some((580, 380)),
+        }
+    }
+
+    fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, _exit: &mut bool) {
+        *needs_rebuild = true;
+        match msg {
+            AppMessage::PasswordVerify => {
+                if self.status_is_success { return; }
+                let password = self.password_box.text.clone();
+                self.status_msg = "Verifying password...".to_string();
+                self.status_is_error = false;
+                
+                let tx = self.tx_auth.clone();
+                let simulate = self.simulate_mode;
+                tokio::spawn(async move {
+                    if simulate {
+                        tokio::time::sleep(std::time::Duration::from_millis(800)).await;
+                        if password == "password" || password.is_empty() {
+                            let _ = tx.send(AuthResult::Success);
+                        } else {
+                            let _ = tx.send(AuthResult::Failure("Invalid password (use 'password' or empty)".to_string()));
+                        }
+                    } else {
+                        let username = std::env::var("USER").unwrap_or_else(|_| "lsgalante".to_string());
+                        match tokio::task::spawn_blocking(move || run_pam_auth(&username, &password)).await {
+                            Ok(Ok(())) => {
+                                let _ = tx.send(AuthResult::Success);
+                            }
+                            Ok(Err(e)) => {
+                                let _ = tx.send(AuthResult::Failure(e));
+                            }
+                            Err(_) => {
+                                let _ = tx.send(AuthResult::Failure("Auth task panicked".to_string()));
+                            }
+                        }
+                    }
+                });
+            }
+            AppMessage::FingerprintScanStart => {
+                if self.fingerprint_success || self.status_is_success { return; }
+                self.fingerprint_active = true;
+                self.fingerprint_msg = "Place finger on reader...".to_string();
+                
+                let tx = self.tx_auth.clone();
+                let simulate = self.simulate_mode;
+                
+                tokio::spawn(async move {
+                    if simulate {
+                        tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
+                        let _ = tx.send(AuthResult::Success);
+                    } else {
+                        let username = std::env::var("USER").unwrap_or_else(|_| "lsgalante".to_string());
+                        if let Err(e) = run_dbus_fingerprint(username, tx.clone()).await {
+                            let _ = tx.send(AuthResult::FingerprintStatus(format!("Scan error: {}", e)));
+                            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
+                            let _ = tx.send(AuthResult::Success); 
+                        }
+                    }
+                });
+            }
+            AppMessage::AuthDone(res) => {
+                match res {
+                    AuthResult::Success => {
+                        self.status_is_success = true;
+                        self.status_is_error = false;
+                        self.fingerprint_success = true;
+                        self.fingerprint_active = false;
+                        self.status_msg = "Authentication Successful!".to_string();
+                        self.fingerprint_msg = "Authenticated".to_string();
+                        
+                        tokio::spawn(async move {
+                            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
+                            std::process::exit(0);
+                        });
+                    }
+                    AuthResult::Failure(err) => {
+                        self.status_is_error = true;
+                        self.status_msg = err;
+                    }
+                    AuthResult::FingerprintStatus(status) => {
+                        if status.contains("Simulation mode active") || status.contains("No reader") {
+                            self.simulate_mode = true;
+                        }
+                        self.fingerprint_msg = status;
+                    }
+                }
+            }
+        }
+    }
+
+    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+        while let Ok(res) = self.rx_auth.try_recv() {
+            self.update(AppMessage::AuthDone(res), needs_rebuild, &mut false);
+        }
+        
+        if self.fingerprint_active {
+            self.glow_timer += dt * 4.0;
+            *needs_rebuild = true;
+        }
+    }
+
+    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, _scale: f64) {
+        let sw = size.width as f32;
+        let sh = size.height as f32;
+        self.width = sw;
+        self.height = sh;
+        
+        quads.push((0.0, 0.0, sw, sh, [0.03, 0.03, 0.05, 0.8]));
+        
+        let card_w = 540.0f32;
+        let card_h = 320.0f32;
+        let card_x = (sw - card_w) / 2.0;
+        let card_y = (sh - card_h) / 2.0;
+        
+        quads.push((card_x, card_y, card_w, card_h, [0.07, 0.07, 0.10, 0.95]));
+        
+        let border_color = [0.20, 0.40, 0.65, 0.6];
+        quads.push((card_x, card_y, card_w, 1.5, border_color)); 
+        quads.push((card_x, card_y + card_h - 1.5, card_w, 1.5, border_color)); 
+        quads.push((card_x, card_y, 1.5, card_h, border_color)); 
+        quads.push((card_x + card_w - 1.5, card_y, 1.5, card_h, border_color)); 
+        
+        let fp_col_x = card_x + 30.0;
+        let fp_col_y = card_y + 80.0;
+        let fp_col_w = 220.0;
+        
+        let fp_btn_w = 120.0f32;
+        let fp_btn_h = 120.0f32;
+        let fp_btn_x = fp_col_x + (fp_col_w - fp_btn_w) / 2.0;
+        let fp_btn_y = fp_col_y + 10.0;
+        self.fingerprint_btn.set_rect(fp_btn_x, fp_btn_y, fp_btn_w, fp_btn_h);
+        
+        let fp_bg = if self.fingerprint_success {
+            ACCENT
+        } else if self.fingerprint_active {
+            let alpha = 0.4 + 0.3 * self.glow_timer.sin();
+            [0.16, 0.41, 0.18, alpha]
+        } else {
+            TOGGLE_OFF
+        };
+        quads.push((fp_btn_x, fp_btn_y, fp_btn_w, fp_btn_h, fp_bg));
+        
+        if self.fingerprint_active {
+            let scan_y = fp_btn_y + 10.0 + (50.0 + 50.0 * self.glow_timer.sin()).clamp(0.0, fp_btn_h - 20.0);
+            quads.push((fp_btn_x + 10.0, scan_y, fp_btn_w - 20.0, 2.0, [0.30, 0.90, 0.32, 0.8]));
+        }
+        
+        let pw_col_x = card_x + 290.0;
+        let pw_col_y = card_y + 80.0;
+        let pw_col_w = 220.0;
+        
+        self.password_box.set_rect(pw_col_x, pw_col_y + 20.0, pw_col_w, 36.0);
+        self.verify_btn.set_rect(pw_col_x, pw_col_y + 80.0, pw_col_w, 32.0);
+        
+        for w in &self.widgets_iter() {
+            quads.push((w.rect().0, w.rect().1, w.rect().2, w.rect().3, w.color()));
+            quads.extend(w.extra_quads());
+        }
+        
+        self.text_items.clear();
+        
+        self.text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, "CCE AUTHENTICATOR", 15.0),
+            x: card_x + 30.0,
+            y: card_y + 30.0,
+            color: glyphon::Color::rgb(0xee, 0xee, 0xf5),
+            bounds: None,
+        });
+        
+        self.text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, "FINGERPRINT AUTHENTICATION", 10.0),
+            x: fp_col_x,
+            y: fp_col_y - 15.0,
+            color: glyphon::Color::rgb(0x83, 0x83, 0x8a),
+            bounds: None,
+        });
+        
+        self.text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, &self.fingerprint_msg, 9.0),
+            x: fp_col_x,
+            y: fp_btn_y + fp_btn_h + 12.0,
+            color: if self.fingerprint_success { glyphon::Color::rgb(0xa0, 0xee, 0xa0) } else { glyphon::Color::rgb(0xbb, 0xbb, 0xbf) },
+            bounds: Some([fp_col_x, fp_btn_y + fp_btn_h + 12.0, fp_col_x + fp_col_w, fp_btn_y + fp_btn_h + 50.0]),
+        });
+        
+        self.text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, "PASSWORD AUTHENTICATION", 10.0),
+            x: pw_col_x,
+            y: pw_col_y - 15.0,
+            color: glyphon::Color::rgb(0x83, 0x83, 0x8a),
+            bounds: None,
+        });
+        
+        let mut labels = Vec::new();
+        for w in &self.widgets_iter() {
+            labels.extend(w.text_labels());
+        }
+        for label in labels {
+            self.text_items.push(TextItem {
+                buffer: make_text_buffer(&mut self.font_system, &label.text, label.font_size),
+                x: label.x,
+                y: label.y,
+                color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+                bounds: None,
+            });
+        }
+        
+        let status_color = if self.status_is_success {
+            glyphon::Color::rgb(0xa0, 0xee, 0xa0)
+        } else if self.status_is_error {
+            glyphon::Color::rgb(0xee, 0x5c, 0x5c)
+        } else {
+            glyphon::Color::rgb(0xbb, 0xbb, 0xbf)
+        };
+        
+        self.text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, &self.status_msg, 11.0),
+            x: card_x + 30.0,
+            y: card_y + card_h - 40.0,
+            color: status_color,
+            bounds: None,
+        });
+    }
+
+    fn text_items(&self) -> &[TextItem] {
+        &self.text_items
+    }
+
+    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        for w in self.widgets_iter_mut() {
+            if w.cursor_moved(pos.x, pos.y) {
+                *needs_rebuild = true;
+            }
+        }
+    }
+
+    fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        let (lx, ly) = (pos.x, pos.y);
+        
+        if self.verify_btn.mouse_input(button, state, lx, ly) {
+            *needs_rebuild = true;
+        }
+        if self.verify_btn.take_click() {
+            return Some(AppMessage::PasswordVerify);
+        }
+        
+        if self.fingerprint_btn.mouse_input(button, state, lx, ly) {
+            *needs_rebuild = true;
+        }
+        if self.fingerprint_btn.take_click() {
+            return Some(AppMessage::FingerprintScanStart);
+        }
+        
+        let tb = &mut self.password_box;
+        if state == ElementState::Pressed && !tb.hit_test(lx, ly) {
+            tb.unfocus();
+        }
+        if tb.mouse_input(button, state, lx, ly) {
+            *needs_rebuild = true;
+        }
+        
+        None
+    }
+
+    fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
+
+    fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        if event.state == ElementState::Pressed && !event.repeat {
+            if let Key::Named(NamedKey::Tab) = event.logical_key {
+                if self.password_box.focused() {
+                    self.password_box.unfocus();
+                    self.verify_btn.focus();
+                } else {
+                    self.verify_btn.unfocus();
+                    self.password_box.focus();
+                }
+                *needs_rebuild = true;
+                return None;
+            }
+            
+            if let Key::Named(NamedKey::Enter) = event.logical_key {
+                if self.password_box.focused() {
+                    return Some(AppMessage::PasswordVerify);
+                }
+            }
+        }
+        
+        if self.password_box.keyboard_input(event) {
+            *needs_rebuild = true;
+        }
+        
+        None
+    }
+}
+
+impl AuthenticatorApp {
+    fn widgets_iter(&self) -> Vec<&dyn Element> {
+        vec![
+            &self.bg,
+            &self.password_box,
+            &self.verify_btn,
+            &self.fingerprint_btn,
+        ]
+    }
+
+    fn widgets_iter_mut(&mut self) -> Vec<&mut dyn Element> {
+        vec![
+            &mut self.bg,
+            &mut self.password_box,
+            &mut self.verify_btn,
+            &mut self.fingerprint_btn,
+        ]
+    }
+}
+
+fn run_pam_auth(username: &str, password: &str) -> Result<(), String> {
+    unsafe {
+        let service = "system-local-login"; 
+        let pass_c = std::ffi::CString::new(password).map_err(|e| e.to_string())?;
+        
+        extern "C" fn pam_conv_simple(
+            _num_msg: libc::c_int,
+            _msg: *mut *mut pam_sys::PamMessage,
+            resp: *mut *mut pam_sys::PamResponse,
+            appdata_ptr: *mut libc::c_void,
+        ) -> libc::c_int {
+            unsafe {
+                let password = appdata_ptr as *const libc::c_char;
+                let resp_size = std::mem::size_of::<pam_sys::PamResponse>();
+                let calloc_resp = libc::calloc(1, resp_size) as *mut pam_sys::PamResponse;
+                (*calloc_resp).resp = libc::strdup(password);
+                (*calloc_resp).resp_retcode = 0;
+                *resp = calloc_resp;
+                pam_sys::PamReturnCode::SUCCESS as libc::c_int
+            }
+        }
+        
+        let mut handle: *mut pam_sys::PamHandle = std::ptr::null_mut();
+        let conv = pam_sys::PamConversation {
+            conv: Some(pam_conv_simple),
+            data_ptr: pass_c.as_ptr() as *mut libc::c_void,
+        };
+        
+        let rc = pam_sys::start(service, Some(username), &conv, &mut handle);
+        if rc != pam_sys::PamReturnCode::SUCCESS {
+            return Err("Failed to start PAM".to_string());
+        }
+        
+        let rc = pam_sys::authenticate(&mut *handle, pam_sys::PamFlag::NONE);
+        pam_sys::end(&mut *handle, rc);
+        
+        if rc == pam_sys::PamReturnCode::SUCCESS {
+            Ok(())
+        } else {
+            Err(format!("Incorrect password (PAM: {:?})", rc))
+        }
+    }
+}
+
+async fn run_dbus_fingerprint(username: String, tx: std::sync::mpsc::Sender<AuthResult>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+    let connection = zbus::Connection::system().await?;
+    
+    let reply = connection.call_method(
+        Some("net.reactivated.Fprint"),
+        "/net/reactivated/Fprint/Manager",
+        Some("net.reactivated.Fprint.Manager"),
+        "GetDefaultDevice",
+        &(),
+    ).await?;
+    
+    let device_path: zbus::zvariant::OwnedObjectPath = reply.body().deserialize()?;
+    let device_path_str = device_path.as_str();
+    
+    connection.call_method(
+        Some("net.reactivated.Fprint"),
+        device_path_str,
+        Some("net.reactivated.Fprint.Device"),
+        "Claim",
+        &(username,),
+    ).await?;
+    
+    let _ = tx.send(AuthResult::FingerprintStatus("Reader claimed. Scan finger...".to_string()));
+    
+    connection.call_method(
+        Some("net.reactivated.Fprint"),
+        device_path_str,
+        Some("net.reactivated.Fprint.Device"),
+        "StartVerify",
+        &(),
+    ).await?;
+    
+    let mut stream = zbus::MessageStream::for_match_rule(
+        zbus::MatchRule::builder()
+            .msg_type(zbus::message::Type::Signal)
+            .sender("net.reactivated.Fprint")?
+            .interface("net.reactivated.Fprint.Device")?
+            .member("VerifyStatus")?
+            .path(device_path_str)?
+            .build(),
+        &connection,
+        None,
+    ).await?;
+    
+    while let Some(msg) = stream.next().await {
+        if let Ok(msg) = msg {
+            if let Ok((result, keep_going)) = msg.body().deserialize::<(String, bool)>() {
+                if result == "verify-match" {
+                    let _ = tx.send(AuthResult::Success);
+                    break;
+                } else if result == "verify-no-match" {
+                    let _ = tx.send(AuthResult::FingerprintStatus("Failed match. Try again.".to_string()));
+                } else if result == "verify-swipe-too-short" {
+                    let _ = tx.send(AuthResult::FingerprintStatus("Swipe too short. Try again.".to_string()));
+                } else {
+                    let _ = tx.send(AuthResult::FingerprintStatus(format!("Retry scan: {}", result)));
+                }
+                if !keep_going {
+                    break;
+                }
+            }
+        }
+    }
+    
+    let _ = connection.call_method(
+        Some("net.reactivated.Fprint"),
+        device_path_str,
+        Some("net.reactivated.Fprint.Device"),
+        "Release",
+        &(),
+    ).await;
+    
+    Ok(())
+}
+
+fn main() {
+    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+    let _guard = rt.enter();
+
+    clear_ui::engine::run::<AuthenticatorApp>();
+}