First propper working version with actual pam

This commit is contained in:
2026-03-05 14:19:08 +01:00
parent 4ed70fd858
commit 76b48f7c70
6 changed files with 235 additions and 100 deletions
+5
View File
@@ -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
+5 -54
View File
@@ -3,63 +3,14 @@ use std::thread;
use log::{debug, error}; use log::{debug, error};
use pam_client::{Context, ErrorCode, Flag}; use pam_client::{Context, ErrorCode, Flag};
use secstr::SecVec;
use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop}; use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop};
use users::get_current_username; use users::get_current_username;
use zeroize::Zeroizing;
const SERVICE_NAME: &str = "wayrustlock"; const SERVICE_NAME: &str = "wayrustlock";
pub struct PasswordBuffer(SecVec<u8>);
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 struct LockConversation {
pub password: Option<PasswordBuffer>, pub password: Option<Zeroizing<String>>,
} }
impl pam_client::ConversationHandler for LockConversation { 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<CString, ErrorCode> { fn prompt_echo_off(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
if let Some(password) = self.password.take() { 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 { } else {
Err(ErrorCode::ABORT) Err(ErrorCode::ABORT)
} }
@@ -84,7 +35,7 @@ impl pam_client::ConversationHandler for LockConversation {
} }
} }
pub fn create_and_run_auth_loop() -> (channel::Sender<PasswordBuffer>, channel::Channel<bool>) { pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channel::Channel<bool>) {
struct AuthLoopState { struct AuthLoopState {
auth_res_send: channel::Sender<bool>, auth_res_send: channel::Sender<bool>,
main_closed: bool, main_closed: bool,
@@ -102,7 +53,7 @@ pub fn create_and_run_auth_loop() -> (channel::Sender<PasswordBuffer>, channel::
.expect("Failed to initialize PAM context"); .expect("Failed to initialize PAM context");
debug!("Prepared to authenticate user '{}'", username); debug!("Prepared to authenticate user '{}'", username);
let (auth_req_send, auth_req_recv) = channel::channel::<PasswordBuffer>(); let (auth_req_send, auth_req_recv) = channel::channel::<Zeroizing<String>>();
let (auth_res_send, auth_res_recv) = channel::channel::<bool>(); let (auth_res_send, auth_res_recv) = channel::channel::<bool>();
thread::spawn(move || { thread::spawn(move || {
+1 -1
View File
@@ -12,7 +12,7 @@ pub struct Config {
#[arg(long)] #[arg(long)]
pub clock: bool, pub clock: bool,
#[arg(long)] #[arg(long, default_value = "true")]
pub indicator: bool, pub indicator: bool,
#[arg(long, default_value = "100")] #[arg(long, default_value = "100")]
+101 -24
View File
@@ -135,6 +135,9 @@ impl LockedSurface {
self.renderer.set_background(background.clone()); self.renderer.set_background(background.clone());
} }
self.renderer
.set_password_display(self.input_handler.get_display_password());
// Render the frame // Render the frame
self.renderer.render(); self.renderer.render();
@@ -171,7 +174,10 @@ impl LockedSurface {
} }
/// Handle a key event from Wayland /// 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<InputAction> {
// Convert to our input handler format // Convert to our input handler format
// Note: KeyEvent has fields: time, raw_code, keysym, utf8 // Note: KeyEvent has fields: time, raw_code, keysym, utf8
// We need to determine state and modifiers from context (not available in this demo) // We need to determine state and modifiers from context (not available in this demo)
@@ -185,32 +191,96 @@ impl LockedSurface {
.handle_key_event(keysym, state, modifiers); .handle_key_event(keysym, state, modifiers);
match action { match action {
InputAction::SubmitPassword(_password) => { InputAction::SubmitPassword(password) => {
// Show key highlight for visual feedback // Show key highlight for visual feedback
self.show_key_highlight(); self.show_key_highlight();
Some(InputAction::SubmitPassword(password))
}
InputAction::Cancel => Some(InputAction::Cancel),
InputAction::TempScreenshot => Some(InputAction::TempScreenshot),
InputAction::PasswordChanged => Some(InputAction::PasswordChanged),
InputAction::None => None,
}
}
// TODO: Authenticate with PAM /// Authenticate a password using PAM
// For now, just clear the password pub fn authenticate_password(&self, password: zeroize::Zeroizing<String>) -> bool {
self.input_handler.clear_password(); // Create a simple PAM conversation that provides the password
struct SimpleConversation {
password: Option<zeroize::Zeroizing<String>>,
}
// TODO: If authentication succeeds, unlock the session impl pam_client::ConversationHandler for SimpleConversation {
// If authentication fails, show wrong password feedback fn init(&mut self, _default_user: Option<impl AsRef<str>>) {}
self.show_wrong_password();
fn prompt_echo_on(
&mut self,
_msg: &std::ffi::CStr,
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
Err(pam_client::ErrorCode::ABORT)
} }
InputAction::Cancel => {
// Escape key pressed fn prompt_echo_off(
// TODO: Handle cancel action (maybe show quit confirmation?) &mut self,
_msg: &std::ffi::CStr,
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
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)
} }
InputAction::TempScreenshot => {
// Temp screenshot (peek) activated
// Already handled in update() method via timer
} }
InputAction::PasswordChanged => {
// Password changed, update display fn text_info(&mut self, _msg: &std::ffi::CStr) {}
// TODO: Update password display in renderer fn error_msg(&mut self, _msg: &std::ffi::CStr) {}
fn radio_prompt(
&mut self,
_msg: &std::ffi::CStr,
) -> Result<bool, pam_client::ErrorCode> {
Ok(false)
} }
InputAction::None => {}
} }
// 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 /// Get the rendered image surface for this locked surface
@@ -246,7 +316,7 @@ impl LockedSurface {
/// Manager for all locked surfaces (multiple outputs) /// Manager for all locked surfaces (multiple outputs)
pub struct LockManager { pub struct LockManager {
surfaces: Vec<LockedSurface>, pub surfaces: Vec<LockedSurface>,
config: Config, config: Config,
locked: bool, locked: bool,
} }
@@ -279,14 +349,21 @@ impl LockManager {
} }
} }
/// Handle a key event (distribute to all surfaces or focused surface) /// Handle a key event and return any action that needs processing
pub fn handle_key_event(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent) { /// Returns the first non-None action from any surface
// For now, send to all surfaces pub fn handle_key_event(
// In a real implementation, we would determine which surface has focus &mut self,
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
) -> Option<InputAction> {
// Distribute key event to all surfaces and collect first action
let mut action = None;
for surface in &mut self.surfaces { 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 /// Check if session is locked
pub fn is_locked(&self) -> bool { pub fn is_locked(&self) -> bool {
+75 -16
View File
@@ -13,6 +13,7 @@ use std::error::Error;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use zeroize::Zeroizing;
fn setup_file_logging() { fn setup_file_logging() {
let log_path = let log_path =
@@ -87,7 +88,7 @@ fn lock_wayland_session(
compositor::{CompositorHandler, CompositorState}, compositor::{CompositorHandler, CompositorState},
output::{OutputHandler, OutputState}, output::{OutputHandler, OutputState},
reexports::{ reexports::{
calloop::{EventLoop, LoopHandle}, calloop::{channel, EventLoop, LoopHandle},
calloop_wayland_source::WaylandSource, calloop_wayland_source::WaylandSource,
}, },
registry::{ProvidesRegistryState, RegistryState}, registry::{ProvidesRegistryState, RegistryState},
@@ -126,6 +127,8 @@ fn lock_wayland_session(
config: Config, config: Config,
ctrlc_exit: Arc<std::sync::atomic::AtomicBool>, ctrlc_exit: Arc<std::sync::atomic::AtomicBool>,
exit: bool, exit: bool,
auth_tx: Option<channel::Sender<Zeroizing<String>>>,
unlocking: bool,
} }
impl SessionLockHandler for WaylandLock { impl SessionLockHandler for WaylandLock {
@@ -170,8 +173,17 @@ fn lock_wayland_session(
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
_session_lock: SessionLock, _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; self.exit = true;
log_to_file("Setting exit=true from finished callback");
} }
fn configure( fn configure(
@@ -474,30 +486,62 @@ fn lock_wayland_session(
impl WaylandLock { impl WaylandLock {
fn handle_key_event(&mut self, event: KeyEvent) { fn handle_key_event(&mut self, event: KeyEvent) {
use crate::input::InputAction;
use smithay_client_toolkit::seat::keyboard::Keysym; use smithay_client_toolkit::seat::keyboard::Keysym;
log_to_file(&format!( log_to_file(&format!(
"handle_key_event called: keysym={:?}", "handle_key_event called: keysym={:?}",
event.keysym event.keysym
)); ));
log::debug!("Key event: {:?}", event.keysym);
if event.keysym == Keysym::Return { if event.keysym == Keysym::Return {
log::info!("Enter pressed - unlocking session (demo mode)"); log::info!("Enter pressed - submitting password for authentication");
log_to_file("ENTER pressed - unlocking!"); log_to_file("ENTER pressed - submitting password");
if let Some(ref session_lock) = self.session_lock.take() {
session_lock.unlock(); 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 { } else {
log::debug!("Non-character keysym pressed: {:?}", event.keysym); // Distribute to all surfaces (BackSpace, characters, etc.)
log_to_file(&format!("Non-char keysym: {:?}", event.keysym)); 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<WaylandLock> = event_queue.handle(); let qh: QueueHandle<WaylandLock> = event_queue.handle();
let mut event_loop: EventLoop<WaylandLock> = EventLoop::try_new()?; let mut event_loop: EventLoop<WaylandLock> = 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 { let mut state = WaylandLock {
loop_handle: event_loop.handle(), loop_handle: event_loop.handle(),
conn: conn.clone(), conn: conn.clone(),
@@ -614,6 +661,8 @@ fn lock_wayland_session(
config, config,
ctrlc_exit: ctrlc_exit.clone(), ctrlc_exit: ctrlc_exit.clone(),
exit: false, exit: false,
auth_tx: Some(auth_tx),
unlocking: false,
}; };
state.session_lock = Some( state.session_lock = Some(
@@ -629,6 +678,16 @@ fn lock_wayland_session(
WaylandSource::new(conn, event_queue).insert(event_loop.handle())?; 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<bool>, _metadata, wayland_lock| {
if let channel::Event::Msg(result) = event {
wayland_lock.handle_auth_result(result);
}
},
)?;
log_to_file("Starting event loop"); log_to_file("Starting event loop");
eprintln!("Starting event loop - press Enter to unlock"); eprintln!("Starting event loop - press Enter to unlock");
+43
View File
@@ -17,6 +17,7 @@ pub struct Renderer {
wrong_password_start: Option<Instant>, wrong_password_start: Option<Instant>,
key_highlight_start: Option<Instant>, key_highlight_start: Option<Instant>,
background: Option<ImageSurface>, background: Option<ImageSurface>,
password_display: String,
} }
impl Renderer { impl Renderer {
@@ -48,6 +49,7 @@ impl Renderer {
wrong_password_start: None, wrong_password_start: None,
key_highlight_start: None, key_highlight_start: None,
background: None, background: None,
password_display: String::new(),
} }
} }
@@ -83,6 +85,11 @@ impl Renderer {
self.key_highlight_start = Some(Instant::now()); 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 /// Render the current frame
pub fn render(&mut self) { pub fn render(&mut self) {
// Clear the surface - draw a VISIBLE color (dark gray) instead of black // Clear the surface - draw a VISIBLE color (dark gray) instead of black
@@ -115,6 +122,11 @@ impl Renderer {
self.draw_indicator(); 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 // Draw wrong password feedback if active
if self.wrong_password_shown { if self.wrong_password_shown {
self.draw_wrong_password_feedback(); 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) /// Draw wrong password feedback (red flash)
fn draw_wrong_password_feedback(&self) { fn draw_wrong_password_feedback(&self) {
let center_x = self.width as f64 / 2.0; let center_x = self.width as f64 / 2.0;