From 76b48f7c70cf238ffc2c7f96ad61ff0ea6080d99 Mon Sep 17 00:00:00 2001 From: JorySeverijnse Date: Thu, 5 Mar 2026 14:19:08 +0100 Subject: [PATCH] First propper working version with actual pam --- pam.d/wayrustlock | 5 ++ src/auth.rs | 59 ++------------------ src/config.rs | 2 +- src/lock.rs | 135 ++++++++++++++++++++++++++++++++++++---------- src/main.rs | 91 +++++++++++++++++++++++++------ src/render.rs | 43 +++++++++++++++ 6 files changed, 235 insertions(+), 100 deletions(-) create mode 100644 pam.d/wayrustlock diff --git a/pam.d/wayrustlock b/pam.d/wayrustlock new file mode 100644 index 0000000..45532b4 --- /dev/null +++ b/pam.d/wayrustlock @@ -0,0 +1,5 @@ +# PAM configuration for wayrustlock +# Install this file to /etc/pam.d/wayrustlock + +# Use the standard login service authentication +auth include login diff --git a/src/auth.rs b/src/auth.rs index 4a371ac..7047f28 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -3,63 +3,14 @@ use std::thread; use log::{debug, error}; use pam_client::{Context, ErrorCode, Flag}; -use secstr::SecVec; use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop}; use users::get_current_username; +use zeroize::Zeroizing; const SERVICE_NAME: &str = "wayrustlock"; -pub struct PasswordBuffer(SecVec); - -impl PasswordBuffer { - pub fn new() -> Self { - Self(SecVec::new(Vec::new())) - } - - fn zeroize_string(mut data: String) { - use std::sync::atomic; - - let default = u8::default(); - - for c in unsafe { data.as_bytes_mut() } { - unsafe { std::ptr::write_volatile(c, default) }; - } - - atomic::fence(atomic::Ordering::SeqCst); - atomic::compiler_fence(atomic::Ordering::SeqCst); - } - - pub fn append(&mut self, data: String) { - let bytes = data.as_bytes(); - let mut og_len = self.0.unsecure().len(); - self.0.resize(og_len + bytes.len(), 0); - for b in bytes { - self.0.unsecure_mut()[og_len] = *b; - og_len += 1; - } - Self::zeroize_string(data); - } - - pub fn backspace(&mut self) { - let og_len = self.0.unsecure().len(); - if og_len != 0 { - self.0.resize(og_len - 1, 0); - } - } - - pub fn unsecure(&self) -> &str { - unsafe { std::str::from_utf8_unchecked(self.0.unsecure()) } - } - - pub fn take(&mut self) -> Self { - let mut new_buffer = SecVec::new(Vec::new()); - std::mem::swap(&mut self.0, &mut new_buffer); - Self(new_buffer) - } -} - pub struct LockConversation { - pub password: Option, + pub password: Option>, } impl pam_client::ConversationHandler for LockConversation { @@ -71,7 +22,7 @@ impl pam_client::ConversationHandler for LockConversation { fn prompt_echo_off(&mut self, _msg: &CStr) -> Result { if let Some(password) = self.password.take() { - CString::new(password.unsecure()).map_err(|_| ErrorCode::ABORT) + CString::new(password.as_str()).map_err(|_| ErrorCode::ABORT) } else { Err(ErrorCode::ABORT) } @@ -84,7 +35,7 @@ impl pam_client::ConversationHandler for LockConversation { } } -pub fn create_and_run_auth_loop() -> (channel::Sender, channel::Channel) { +pub fn create_and_run_auth_loop() -> (channel::Sender>, channel::Channel) { struct AuthLoopState { auth_res_send: channel::Sender, main_closed: bool, @@ -102,7 +53,7 @@ pub fn create_and_run_auth_loop() -> (channel::Sender, channel:: .expect("Failed to initialize PAM context"); debug!("Prepared to authenticate user '{}'", username); - let (auth_req_send, auth_req_recv) = channel::channel::(); + let (auth_req_send, auth_req_recv) = channel::channel::>(); let (auth_res_send, auth_res_recv) = channel::channel::(); thread::spawn(move || { diff --git a/src/config.rs b/src/config.rs index d562e20..36275cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ pub struct Config { #[arg(long)] pub clock: bool, - #[arg(long)] + #[arg(long, default_value = "true")] pub indicator: bool, #[arg(long, default_value = "100")] diff --git a/src/lock.rs b/src/lock.rs index 0005eb9..40a929f 100644 --- a/src/lock.rs +++ b/src/lock.rs @@ -135,6 +135,9 @@ impl LockedSurface { self.renderer.set_background(background.clone()); } + self.renderer + .set_password_display(self.input_handler.get_display_password()); + // Render the frame self.renderer.render(); @@ -171,7 +174,10 @@ impl LockedSurface { } /// Handle a key event from Wayland - pub fn handle_key_event(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent) { + pub fn handle_key_event( + &mut self, + event: smithay_client_toolkit::seat::keyboard::KeyEvent, + ) -> Option { // Convert to our input handler format // Note: KeyEvent has fields: time, raw_code, keysym, utf8 // We need to determine state and modifiers from context (not available in this demo) @@ -185,34 +191,98 @@ impl LockedSurface { .handle_key_event(keysym, state, modifiers); match action { - InputAction::SubmitPassword(_password) => { + InputAction::SubmitPassword(password) => { // Show key highlight for visual feedback self.show_key_highlight(); - - // TODO: Authenticate with PAM - // For now, just clear the password - self.input_handler.clear_password(); - - // TODO: If authentication succeeds, unlock the session - // If authentication fails, show wrong password feedback - self.show_wrong_password(); + Some(InputAction::SubmitPassword(password)) } - InputAction::Cancel => { - // Escape key pressed - // TODO: Handle cancel action (maybe show quit confirmation?) - } - InputAction::TempScreenshot => { - // Temp screenshot (peek) activated - // Already handled in update() method via timer - } - InputAction::PasswordChanged => { - // Password changed, update display - // TODO: Update password display in renderer - } - InputAction::None => {} + InputAction::Cancel => Some(InputAction::Cancel), + InputAction::TempScreenshot => Some(InputAction::TempScreenshot), + InputAction::PasswordChanged => Some(InputAction::PasswordChanged), + InputAction::None => None, } } + /// Authenticate a password using PAM + pub fn authenticate_password(&self, password: zeroize::Zeroizing) -> bool { + // Create a simple PAM conversation that provides the password + struct SimpleConversation { + password: Option>, + } + + impl pam_client::ConversationHandler for SimpleConversation { + fn init(&mut self, _default_user: Option>) {} + + fn prompt_echo_on( + &mut self, + _msg: &std::ffi::CStr, + ) -> Result { + Err(pam_client::ErrorCode::ABORT) + } + + fn prompt_echo_off( + &mut self, + _msg: &std::ffi::CStr, + ) -> Result { + if let Some(pwd) = self.password.take() { + std::ffi::CString::new(pwd.as_str()).map_err(|_| pam_client::ErrorCode::ABORT) + } else { + Err(pam_client::ErrorCode::ABORT) + } + } + + fn text_info(&mut self, _msg: &std::ffi::CStr) {} + fn error_msg(&mut self, _msg: &std::ffi::CStr) {} + fn radio_prompt( + &mut self, + _msg: &std::ffi::CStr, + ) -> Result { + Ok(false) + } + } + + // Get username + let username = match users::get_current_username() { + Some(name) => name.to_string_lossy().into_owned(), + None => { + log::error!("Failed to get current username"); + return false; + } + }; + + // Create PAM context + let service_name = &self.config.pam_service; + let conversation = SimpleConversation { + password: Some(password), + }; + + let mut context = + match pam_client::Context::new(service_name, Some(username.as_str()), conversation) { + Ok(ctx) => ctx, + Err(e) => { + log::error!("Failed to initialize PAM context: {:?}", e); + return false; + } + }; + + // Authenticate + match context.authenticate(pam_client::Flag::NONE) { + Ok(()) => { + log::info!("PAM authentication successful for user {}", username); + true + } + Err(e) => { + log::warn!("PAM authentication failed: {:?}", e); + false + } + } + } + + /// Get the input handler for this locked surface + pub fn input_handler(&self) -> &InputHandler { + &self.input_handler + } + /// Get the rendered image surface for this locked surface pub fn as_image_surface(&self) -> &ImageSurface { self.renderer.as_image_surface() @@ -246,7 +316,7 @@ impl LockedSurface { /// Manager for all locked surfaces (multiple outputs) pub struct LockManager { - surfaces: Vec, + pub surfaces: Vec, config: Config, locked: bool, } @@ -279,13 +349,20 @@ impl LockManager { } } - /// Handle a key event (distribute to all surfaces or focused surface) - pub fn handle_key_event(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent) { - // For now, send to all surfaces - // In a real implementation, we would determine which surface has focus + /// Handle a key event and return any action that needs processing + /// Returns the first non-None action from any surface + pub fn handle_key_event( + &mut self, + event: smithay_client_toolkit::seat::keyboard::KeyEvent, + ) -> Option { + // Distribute key event to all surfaces and collect first action + let mut action = None; for surface in &mut self.surfaces { - surface.handle_key_event(event.clone()); + if let Some(a) = surface.handle_key_event(event.clone()) { + action = Some(a); + } } + action } /// Check if session is locked diff --git a/src/main.rs b/src/main.rs index 9fd0949..e5103d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use std::error::Error; use std::fs::OpenOptions; use std::io::Write; use std::sync::{Arc, Mutex}; +use zeroize::Zeroizing; fn setup_file_logging() { let log_path = @@ -87,7 +88,7 @@ fn lock_wayland_session( compositor::{CompositorHandler, CompositorState}, output::{OutputHandler, OutputState}, reexports::{ - calloop::{EventLoop, LoopHandle}, + calloop::{channel, EventLoop, LoopHandle}, calloop_wayland_source::WaylandSource, }, registry::{ProvidesRegistryState, RegistryState}, @@ -126,6 +127,8 @@ fn lock_wayland_session( config: Config, ctrlc_exit: Arc, exit: bool, + auth_tx: Option>>, + unlocking: bool, } impl SessionLockHandler for WaylandLock { @@ -170,8 +173,17 @@ fn lock_wayland_session( _qh: &QueueHandle, _session_lock: SessionLock, ) { - log::info!("Session unlocked or lock denied - exiting"); + if self.unlocking { + log::info!("✅ Session successfully unlocked - compositor confirmed"); + log_to_file("✅ Session successfully unlocked - compositor confirmed"); + self.unlocking = false; + } else { + log::warn!("Session finished without unlock request - lock denied or cancelled"); + log_to_file("Session finished without unlock request"); + } + self.session_lock = None; self.exit = true; + log_to_file("Setting exit=true from finished callback"); } fn configure( @@ -474,30 +486,62 @@ fn lock_wayland_session( impl WaylandLock { fn handle_key_event(&mut self, event: KeyEvent) { + use crate::input::InputAction; use smithay_client_toolkit::seat::keyboard::Keysym; log_to_file(&format!( "handle_key_event called: keysym={:?}", event.keysym )); + log::debug!("Key event: {:?}", event.keysym); if event.keysym == Keysym::Return { - log::info!("Enter pressed - unlocking session (demo mode)"); - log_to_file("ENTER pressed - unlocking!"); - if let Some(ref session_lock) = self.session_lock.take() { - session_lock.unlock(); + log::info!("Enter pressed - submitting password for authentication"); + log_to_file("ENTER pressed - submitting password"); + + if let Ok(mut lock_manager) = self.lock_manager.lock() { + if let Some(InputAction::SubmitPassword(password)) = + lock_manager.handle_key_event(event.clone()) + { + // Send password to auth thread for processing + if let Some(tx) = &self.auth_tx { + let _ = tx.send(password); + } + log::debug!("Password submitted for authentication"); + } } - self.exit = true; - log_to_file("Set exit=true"); - } else if event.keysym == Keysym::BackSpace { - log::info!("Backspace pressed"); - log_to_file("Backspace pressed"); - } else if let Some(input) = event.utf8 { - log::info!("Key pressed: '{}'", input); - log_to_file(&format!("Key pressed: '{}'", input)); } else { - log::debug!("Non-character keysym pressed: {:?}", event.keysym); - log_to_file(&format!("Non-char keysym: {:?}", event.keysym)); + // Distribute to all surfaces (BackSpace, characters, etc.) + let _ = self + .lock_manager + .lock() + .map(|mut lm| lm.handle_key_event(event)); + } + } + + /// Handle authentication result from the auth thread + fn handle_auth_result(&mut self, success: bool) { + if success { + log::info!("✅ Authentication successful - unlocking session"); + log_to_file("✅ Authentication successful - unlocking session"); + + // Call unlock on the session_lock, but keep it to receive finished event + if let Some(session_lock) = &self.session_lock { + session_lock.unlock(); + self.unlocking = true; + log_to_file("Unlock requested - waiting for finished event"); + } else { + log::error!("No session_lock available to unlock!"); + } + } else { + log::warn!("❌ Authentication failed - wrong password"); + log_to_file("❌ Authentication failed - wrong password"); + // Show wrong password feedback on all surfaces + if let Ok(mut lock_manager) = self.lock_manager.lock() { + for surface in &mut lock_manager.surfaces { + surface.show_wrong_password(); + } + } } } } @@ -593,6 +637,9 @@ fn lock_wayland_session( let qh: QueueHandle = event_queue.handle(); let mut event_loop: EventLoop = EventLoop::try_new()?; + // Create the authentication loop in a separate thread + let (auth_tx, auth_rx) = auth::create_and_run_auth_loop(); + let mut state = WaylandLock { loop_handle: event_loop.handle(), conn: conn.clone(), @@ -614,6 +661,8 @@ fn lock_wayland_session( config, ctrlc_exit: ctrlc_exit.clone(), exit: false, + auth_tx: Some(auth_tx), + unlocking: false, }; state.session_lock = Some( @@ -629,6 +678,16 @@ fn lock_wayland_session( WaylandSource::new(conn, event_queue).insert(event_loop.handle())?; + // Insert the authentication channel into the event loop + event_loop.handle().insert_source( + auth_rx, + |event: channel::Event, _metadata, wayland_lock| { + if let channel::Event::Msg(result) = event { + wayland_lock.handle_auth_result(result); + } + }, + )?; + log_to_file("Starting event loop"); eprintln!("Starting event loop - press Enter to unlock"); diff --git a/src/render.rs b/src/render.rs index 066e057..16660b0 100644 --- a/src/render.rs +++ b/src/render.rs @@ -17,6 +17,7 @@ pub struct Renderer { wrong_password_start: Option, key_highlight_start: Option, background: Option, + password_display: String, } impl Renderer { @@ -48,6 +49,7 @@ impl Renderer { wrong_password_start: None, key_highlight_start: None, background: None, + password_display: String::new(), } } @@ -83,6 +85,11 @@ impl Renderer { self.key_highlight_start = Some(Instant::now()); } + /// Set the password display string (masked) + pub fn set_password_display(&mut self, password: String) { + self.password_display = password; + } + /// Render the current frame pub fn render(&mut self) { // Clear the surface - draw a VISIBLE color (dark gray) instead of black @@ -115,6 +122,11 @@ impl Renderer { self.draw_indicator(); } + // Draw password display (if not empty) + if !self.password_display.is_empty() { + self.draw_password_display(); + } + // Draw wrong password feedback if active if self.wrong_password_shown { self.draw_wrong_password_feedback(); @@ -243,6 +255,37 @@ impl Renderer { } } + /// Draw the password display (masked characters) + fn draw_password_display(&self) { + if self.password_display.is_empty() { + return; + } + + // Position: below the indicator ring (or centered if no indicator) + let center_x = self.width as f64 / 2.0; + let center_y = self.height as f64 / 2.0; + let radius = self.config.indicator_radius as f64; + let thickness = self.config.indicator_thickness as f64; + + // Place password text below the ring + let text_y = center_y + radius + thickness + 40.0; // 40px below ring + + self.context.set_font_size(36.0); + self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha); + + // Center the text + let extents = self + .context + .text_extents(&self.password_display) + .expect("Failed to get password text extents"); + let text_x = center_x - extents.width() / 2.0; + + self.context.move_to(text_x, text_y); + self.context + .show_text(&self.password_display) + .expect("Failed to draw password"); + } + /// Draw wrong password feedback (red flash) fn draw_wrong_password_feedback(&self) { let center_x = self.width as f64 / 2.0;