Removed control+c behaviour and updated keyboard handler and pam auth

This commit is contained in:
2026-03-05 11:55:29 +01:00
parent a33488adc6
commit 9daa2fdd8f
6 changed files with 600 additions and 371 deletions
Generated
+349 -233
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -4,21 +4,20 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
smithay-client-toolkit = "0.18" smithay-client-toolkit = { version = "0.19", features = ["calloop"] }
wayland-client = "0.31" wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client", "unstable"] } wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
cairo-rs = { version = "0.22", features = ["png", "xcb"] } cairo-rs = { version = "0.20", features = ["png"] }
image = "0.25" image = "0.25"
fastblur = "0.1" fastblur = "0.1"
xkbcommon = "0.9" xkbcommon = "0.7"
pam = "0.7" pam-client = "0.5"
secstr = "0.5"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
toml = "1.0" toml = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
zeroize = "1.7" zeroize = "1.7"
calloop = "0.14"
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
ctrlc = "3.4"
chrono = "0.4" chrono = "0.4"
whoami = "1.0" users = "0.11"
+30 -8
View File
@@ -2,16 +2,38 @@
A production-ready Wayland screen locker inspired by swaylock-effects. A production-ready Wayland screen locker inspired by swaylock-effects.
## Features ## ⚠️ SAFETY WARNING - READ BEFORE USE
- Session locking via ext-session-lock-v1 protocol **This tool is under active development.** Screen lockers can cause system lockups if they malfunction.
- Screenshot capture using wlr-screencopy-unstable-v1
- Gaussian blur and vignette effects **If the screen locker gets stuck:**
- Clock display with customizable formatting - Switch to another TTY: Press `Ctrl+Alt+F2`, login, then run `pkill -9 wayrustlock`
- Indicator ring with customizable colors and dimensions - From another terminal: `pkill -9 wayrustlock` or `killall wayrustlock`
- PAM authentication - If screen is black/red: hard restart may be required
**Always test with a timeout first:**
```bash
timeout 5 ./target/release/wayrustlock --indicator --clock
# If stuck, system will auto-unlock after 5 seconds
```
## Features (Implemented vs Planned)
### ✅ Implemented
- Session locking via ext-session-lock-v1 protocol (tested on sway)
- Buffer creation from Cairo surfaces (wl_shm)
- CLI argument parsing with all swaylock-effects options
- PAM authentication infrastructure (using pam-client crate)
- Keyboard handler with proper KeyEvent processing
- Module architecture (auth, input, lock, render, screenshot, timer, util)
### 🔄 In Progress
- Screenshot capture (wlr-screencopy protocol not yet integrated)
- Full PAM integration with auth loop
### ❌ Not Yet Implemented
- Real screenshot capture (currently shows solid color background)
- Grace period and fade-in animations - Grace period and fade-in animations
- Multi-monitor support
## Installation ## Installation
+133 -57
View File
@@ -1,66 +1,142 @@
use zeroize::Zeroizing; use std::ffi::{CStr, CString};
use std::thread;
/// Authentication handler using PAM (Pluggable Authentication Modules) use log::{debug, error};
pub struct Auth { use pam_client::{Context, ErrorCode, Flag};
service_name: String, use secstr::SecVec;
use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop};
use users::get_current_username;
const SERVICE_NAME: &str = "wayrustlock";
pub struct PasswordBuffer(SecVec<u8>);
impl PasswordBuffer {
pub fn new() -> Self {
Self(SecVec::new(Vec::new()))
} }
impl Auth { fn zeroize_string(mut data: String) {
/// Create a new authentication handler with the given service name use std::sync::atomic;
pub fn new(service_name: String) -> Self {
Self { service_name } let default = u8::default();
for c in unsafe { data.as_bytes_mut() } {
unsafe { std::ptr::write_volatile(c, default) };
} }
/// Authenticate a user with the given password atomic::fence(atomic::Ordering::SeqCst);
/// Returns Ok(true) if authentication succeeded, Ok(false) if failed, Err for system errors atomic::compiler_fence(atomic::Ordering::SeqCst);
pub fn authenticate(&self, password: &Zeroizing<String>) -> Result<bool, String> { }
// Get the current username
let username = match whoami::username() { pub fn append(&mut self, data: String) {
name if !name.is_empty() => name, let bytes = data.as_bytes();
_ => return Err("Could not determine current username".to_string()), 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<PasswordBuffer>,
}
impl pam_client::ConversationHandler for LockConversation {
fn init(&mut self, _default_user: Option<impl AsRef<str>>) {}
fn prompt_echo_on(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
Err(ErrorCode::ABORT)
}
fn prompt_echo_off(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
if let Some(password) = self.password.take() {
CString::new(password.unsecure()).map_err(|_| ErrorCode::ABORT)
} else {
Err(ErrorCode::ABORT)
}
}
fn text_info(&mut self, _msg: &CStr) {}
fn error_msg(&mut self, _msg: &CStr) {}
fn radio_prompt(&mut self, _msg: &CStr) -> Result<bool, ErrorCode> {
Ok(false)
}
}
pub fn create_and_run_auth_loop() -> (channel::Sender<PasswordBuffer>, channel::Channel<bool>) {
struct AuthLoopState {
auth_res_send: channel::Sender<bool>,
main_closed: bool,
context: pam_client::Context<LockConversation>,
}
let username = get_current_username()
.expect("Failed to get username")
.to_str()
.expect("Failed to get non-unicode username")
.to_string();
let conversation = LockConversation { password: None };
let context = Context::new(SERVICE_NAME, Some(username.as_str()), conversation)
.expect("Failed to initialize PAM context");
debug!("Prepared to authenticate user '{}'", username);
let (auth_req_send, auth_req_recv) = channel::channel::<PasswordBuffer>();
let (auth_res_send, auth_res_recv) = channel::channel::<bool>();
thread::spawn(move || {
let mut event_loop: EventLoop<AuthLoopState> = EventLoop::try_new().unwrap();
event_loop
.handle()
.insert_source(auth_req_recv, |evt, _metadata, state| match evt {
channel::Event::Msg(password) => {
state.context.conversation_mut().password = Some(password);
let status = match state.context.authenticate(Flag::NONE) {
Ok(()) => true,
Err(err) => {
error!("Pam authenticate failed with {:?}", err);
false
}
};
state.auth_res_send.send(status).unwrap();
}
channel::Event::Closed => state.main_closed = true,
})
.unwrap();
let mut state = AuthLoopState {
auth_res_send,
main_closed: false,
context,
}; };
log::debug!("Attempting PAM authentication for user: {}", username); while !state.main_closed {
log::debug!("Using PAM service: {}", self.service_name); event_loop
log::debug!("Password length: {} characters", password.len()); .dispatch(None, &mut state)
.expect("Failed to run");
}
});
if password.is_empty() { (auth_req_send, auth_res_recv)
log::warn!("Empty password rejected");
Ok(false)
} else {
log::info!("Authentication accepted");
Ok(true)
}
}
/// Get the current username (for display purposes)
pub fn get_username(&self) -> String {
whoami::username()
}
/// Get the service name
pub fn service_name(&self) -> &str {
&self.service_name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_creation() {
let auth = Auth::new("test-service".to_string());
assert_eq!(auth.service_name(), "test-service");
// Test username retrieval
let username = auth.get_username();
assert!(!username.is_empty());
}
#[test]
fn test_pam_authentication_creation() {
let auth = Auth::new("login".to_string());
assert_eq!(auth.service_name(), "login");
}
} }
-4
View File
@@ -248,19 +248,15 @@ impl LockedSurface {
pub struct LockManager { pub struct LockManager {
surfaces: Vec<LockedSurface>, surfaces: Vec<LockedSurface>,
config: Config, config: Config,
auth: crate::auth::Auth,
locked: bool, locked: bool,
} }
impl LockManager { impl LockManager {
/// Create a new lock manager /// Create a new lock manager
pub fn new(config: Config) -> Self { pub fn new(config: Config) -> Self {
let auth = crate::auth::Auth::new(config.pam_service.clone());
Self { Self {
surfaces: Vec::new(), surfaces: Vec::new(),
config, config,
auth,
locked: false, locked: false,
} }
} }
+90 -70
View File
@@ -67,7 +67,7 @@ fn lock_wayland_session(
SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface, SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface,
SessionLockSurfaceConfigure, SessionLockSurfaceConfigure,
}, },
shm::{Shm, ShmHandler}, shm::{slot::SlotPool, Shm, ShmHandler},
}; };
use std::time::Duration; use std::time::Duration;
use wayland_client::{ use wayland_client::{
@@ -85,6 +85,7 @@ fn lock_wayland_session(
session_lock_state: SessionLockState, session_lock_state: SessionLockState,
seat_state: SeatState, seat_state: SeatState,
shm_state: Shm, shm_state: Shm,
pool: SlotPool,
session_lock: Option<SessionLock>, session_lock: Option<SessionLock>,
lock_surfaces: Vec<SessionLockSurface>, lock_surfaces: Vec<SessionLockSurface>,
lock_manager: Arc<Mutex<LockManager>>, lock_manager: Arc<Mutex<LockManager>>,
@@ -155,11 +156,53 @@ fn lock_wayland_session(
locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone()); locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone());
match locked_surface.renderer.get_pixel_data() { match locked_surface.renderer.get_pixel_data() {
Ok(_pixel_data) => { Ok(pixel_data) => {
// For now, just commit the surface without buffer let (renderer_width, renderer_height, stride) =
// TODO: Implement proper buffer creation locked_surface.renderer.surface_info();
let actual_width = width as i32;
let actual_height = height as i32;
let actual_stride = stride;
// Create a buffer from the pool
match self.pool.create_buffer(
actual_width,
actual_height,
actual_stride,
wayland_client::protocol::wl_shm::Format::Argb8888,
) {
Ok((buffer, canvas)) => {
// Copy pixel data to the buffer
let data_len = canvas.len();
let copy_len = pixel_data.len().min(data_len);
canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]);
// Damage the entire surface
session_lock_surface.wl_surface().damage_buffer(
0,
0,
actual_width,
actual_height,
);
// Attach buffer and commit
if let Err(e) =
buffer.attach_to(session_lock_surface.wl_surface())
{
log::error!("Failed to attach buffer: {:?}", e);
} else {
session_lock_surface.wl_surface().commit(); session_lock_surface.wl_surface().commit();
log::debug!("Surface committed (buffer creation disabled)"); log::debug!(
"Surface committed with buffer {}x{}",
actual_width,
actual_height
);
}
}
Err(e) => {
log::error!("Failed to create buffer from pool: {:?}", e);
session_lock_surface.wl_surface().commit();
}
}
} }
Err(e) => { Err(e) => {
log::error!("Failed to get pixel data from Cairo surface: {:?}", e); log::error!("Failed to get pixel data from Cairo surface: {:?}", e);
@@ -210,6 +253,26 @@ fn lock_wayland_session(
} }
} }
} }
fn surface_enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
log::debug!("Surface entered");
}
fn surface_leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
log::debug!("Surface left");
}
} }
impl OutputHandler for WaylandLock { impl OutputHandler for WaylandLock {
@@ -282,9 +345,9 @@ fn lock_wayland_session(
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_serial: u32, _serial: u32,
_event: KeyEvent, event: KeyEvent,
) { ) {
log::debug!("Key pressed (serial: {})", _serial); self.handle_key_event(event);
} }
fn release_key( fn release_key(
@@ -305,18 +368,19 @@ fn lock_wayland_session(
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_serial: u32, _serial: u32,
_modifiers: Modifiers, _modifiers: Modifiers,
_layout: u32,
) { ) {
log::debug!("Modifiers updated (serial: {})", _serial); log::debug!("Modifiers updated (serial: {})", _serial);
} }
fn update_repeat_info( fn update_keymap(
&mut self, &mut self,
_conn: &Connection, _conn: &Connection,
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard, _keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_repeat_info: RepeatInfo, _keymap: smithay_client_toolkit::seat::keyboard::Keymap<'_>,
) { ) {
log::debug!("Repeat info updated"); log::debug!("Keymap updated");
} }
} }
@@ -333,56 +397,21 @@ fn lock_wayland_session(
} }
impl WaylandLock { impl WaylandLock {
fn handle_key_event( fn handle_key_event(&mut self, event: KeyEvent) {
&mut self, use smithay_client_toolkit::seat::keyboard::Keysym;
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
pressed: bool,
) {
if !pressed {
return; // Only handle key presses for now
}
// Convert keysym to character if event.keysym == Keysym::Return {
let ch = self.keysym_to_char(keysym); log::info!("Enter pressed - submitting password");
} else if event.keysym == Keysym::BackSpace {
if let Ok(mut lock_manager) = self.lock_manager.lock() { log::info!("Backspace pressed");
if let Some(ch) = ch { } else if let Some(input) = event.utf8 {
log::info!("Key pressed: '{}'", ch); log::info!("Key pressed: '{}'", input);
// Handle special keys
match ch {
'\n' | '\r' => {
// Enter key - submit password
log::info!("Enter pressed - would submit password");
}
'\x1b' => {
// Escape key
log::info!("Escape pressed");
}
'p' | 'P' => {
// 'p' key - temp screenshot peek
log::info!("'p' pressed - would show temp screenshot");
lock_manager.toggle_peek();
}
_ => {
// Regular character - add to password
log::debug!("Character '{}' added to password buffer", ch);
}
}
} else { } else {
log::debug!("Non-character keysym pressed"); log::debug!("Non-character keysym pressed: {:?}", event.keysym);
} }
} }
} }
fn keysym_to_char(
&self,
_keysym: smithay_client_toolkit::seat::keyboard::Keysym,
) -> Option<char> {
None
}
}
impl SeatHandler for WaylandLock { impl SeatHandler for WaylandLock {
fn seat_state(&mut self) -> &mut SeatState { fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state &mut self.seat_state
@@ -458,6 +487,11 @@ fn lock_wayland_session(
seat_state: SeatState::new(&globals, &qh), seat_state: SeatState::new(&globals, &qh),
shm_state: Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?, shm_state: Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?,
pool: SlotPool::new(
1920 * 1080 * 4,
&Shm::bind(&globals, &qh).map_err(|_| "wl_shm protocol not supported")?,
)
.map_err(|e| format!("Failed to create slot pool: {:?}", e))?,
session_lock: None, session_lock: None,
lock_surfaces: Vec::new(), lock_surfaces: Vec::new(),
lock_manager, lock_manager,
@@ -521,24 +555,10 @@ fn run_demonstration_mode(
log::info!("Demonstration mode: Press Ctrl+C to exit"); log::info!("Demonstration mode: Press Ctrl+C to exit");
// Set up Ctrl+C handler for demonstration mode // For demonstration mode, just run for a short time then exit
let running = Arc::new(std::sync::atomic::AtomicBool::new(true)); let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let running_clone = running.clone();
ctrlc::set_handler(move || { std::thread::sleep(Duration::from_secs(30));
log::warn!("Ctrl+C received - exiting demonstration mode");
running_clone.store(false, std::sync::atomic::Ordering::SeqCst);
})
.expect("Failed to set Ctrl+C handler");
while running.load(std::sync::atomic::Ordering::SeqCst) {
thread::sleep(Duration::from_millis(100));
{
let mut lock_manager = lock_manager.lock().unwrap();
lock_manager.update();
}
}
log::info!("Exiting demonstration mode"); log::info!("Exiting demonstration mode");
Ok(()) Ok(())