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"
[dependencies]
smithay-client-toolkit = "0.18"
smithay-client-toolkit = { version = "0.19", features = ["calloop"] }
wayland-client = "0.31"
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"
fastblur = "0.1"
xkbcommon = "0.9"
pam = "0.7"
xkbcommon = "0.7"
pam-client = "0.5"
secstr = "0.5"
clap = { version = "4.5", features = ["derive"] }
toml = "1.0"
serde = { version = "1.0", features = ["derive"] }
zeroize = "1.7"
calloop = "0.14"
log = "0.4"
env_logger = "0.11"
ctrlc = "3.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.
## Features
## ⚠️ SAFETY WARNING - READ BEFORE USE
- Session locking via ext-session-lock-v1 protocol
- Screenshot capture using wlr-screencopy-unstable-v1
- Gaussian blur and vignette effects
- Clock display with customizable formatting
- Indicator ring with customizable colors and dimensions
- PAM authentication
**This tool is under active development.** Screen lockers can cause system lockups if they malfunction.
**If the screen locker gets stuck:**
- Switch to another TTY: Press `Ctrl+Alt+F2`, login, then run `pkill -9 wayrustlock`
- From another terminal: `pkill -9 wayrustlock` or `killall wayrustlock`
- 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
- Multi-monitor support
## Installation
+122 -46
View File
@@ -1,66 +1,142 @@
use zeroize::Zeroizing;
use std::ffi::{CStr, CString};
use std::thread;
/// Authentication handler using PAM (Pluggable Authentication Modules)
pub struct Auth {
service_name: String,
}
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;
impl Auth {
/// Create a new authentication handler with the given service name
pub fn new(service_name: String) -> Self {
Self { service_name }
const SERVICE_NAME: &str = "wayrustlock";
pub struct PasswordBuffer(SecVec<u8>);
impl PasswordBuffer {
pub fn new() -> Self {
Self(SecVec::new(Vec::new()))
}
/// Authenticate a user with the given password
/// Returns Ok(true) if authentication succeeded, Ok(false) if failed, Err for system errors
pub fn authenticate(&self, password: &Zeroizing<String>) -> Result<bool, String> {
// Get the current username
let username = match whoami::username() {
name if !name.is_empty() => name,
_ => return Err("Could not determine current username".to_string()),
};
fn zeroize_string(mut data: String) {
use std::sync::atomic;
log::debug!("Attempting PAM authentication for user: {}", username);
log::debug!("Using PAM service: {}", self.service_name);
log::debug!("Password length: {} characters", password.len());
let default = u8::default();
if password.is_empty() {
log::warn!("Empty password rejected");
Ok(false)
} else {
log::info!("Authentication accepted");
Ok(true)
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);
}
}
/// Get the current username (for display purposes)
pub fn get_username(&self) -> String {
whoami::username()
pub fn unsecure(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.0.unsecure()) }
}
/// Get the service name
pub fn service_name(&self) -> &str {
&self.service_name
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
pub struct LockConversation {
pub password: Option<PasswordBuffer>,
}
#[test]
fn test_auth_creation() {
let auth = Auth::new("test-service".to_string());
assert_eq!(auth.service_name(), "test-service");
impl pam_client::ConversationHandler for LockConversation {
fn init(&mut self, _default_user: Option<impl AsRef<str>>) {}
// Test username retrieval
let username = auth.get_username();
assert!(!username.is_empty());
fn prompt_echo_on(&mut self, _msg: &CStr) -> Result<CString, ErrorCode> {
Err(ErrorCode::ABORT)
}
#[test]
fn test_pam_authentication_creation() {
let auth = Auth::new("login".to_string());
assert_eq!(auth.service_name(), "login");
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,
};
while !state.main_closed {
event_loop
.dispatch(None, &mut state)
.expect("Failed to run");
}
});
(auth_req_send, auth_res_recv)
}
-4
View File
@@ -248,19 +248,15 @@ impl LockedSurface {
pub struct LockManager {
surfaces: Vec<LockedSurface>,
config: Config,
auth: crate::auth::Auth,
locked: bool,
}
impl LockManager {
/// Create a new lock manager
pub fn new(config: Config) -> Self {
let auth = crate::auth::Auth::new(config.pam_service.clone());
Self {
surfaces: Vec::new(),
config,
auth,
locked: false,
}
}
+93 -73
View File
@@ -67,7 +67,7 @@ fn lock_wayland_session(
SessionLock, SessionLockHandler, SessionLockState, SessionLockSurface,
SessionLockSurfaceConfigure,
},
shm::{Shm, ShmHandler},
shm::{slot::SlotPool, Shm, ShmHandler},
};
use std::time::Duration;
use wayland_client::{
@@ -85,6 +85,7 @@ fn lock_wayland_session(
session_lock_state: SessionLockState,
seat_state: SeatState,
shm_state: Shm,
pool: SlotPool,
session_lock: Option<SessionLock>,
lock_surfaces: Vec<SessionLockSurface>,
lock_manager: Arc<Mutex<LockManager>>,
@@ -155,11 +156,53 @@ fn lock_wayland_session(
locked_surface.set_wayland_surface(session_lock_surface.wl_surface().clone());
match locked_surface.renderer.get_pixel_data() {
Ok(_pixel_data) => {
// For now, just commit the surface without buffer
// TODO: Implement proper buffer creation
session_lock_surface.wl_surface().commit();
log::debug!("Surface committed (buffer creation disabled)");
Ok(pixel_data) => {
let (renderer_width, renderer_height, stride) =
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();
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) => {
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 {
@@ -282,9 +345,9 @@ fn lock_wayland_session(
_qh: &QueueHandle<Self>,
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_serial: u32,
_event: KeyEvent,
event: KeyEvent,
) {
log::debug!("Key pressed (serial: {})", _serial);
self.handle_key_event(event);
}
fn release_key(
@@ -305,18 +368,19 @@ fn lock_wayland_session(
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
_serial: u32,
_modifiers: Modifiers,
_layout: u32,
) {
log::debug!("Modifiers updated (serial: {})", _serial);
}
fn update_repeat_info(
fn update_keymap(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_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,53 +397,18 @@ fn lock_wayland_session(
}
impl WaylandLock {
fn handle_key_event(
&mut self,
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
pressed: bool,
) {
if !pressed {
return; // Only handle key presses for now
fn handle_key_event(&mut self, event: KeyEvent) {
use smithay_client_toolkit::seat::keyboard::Keysym;
if event.keysym == Keysym::Return {
log::info!("Enter pressed - submitting password");
} else if event.keysym == Keysym::BackSpace {
log::info!("Backspace pressed");
} else if let Some(input) = event.utf8 {
log::info!("Key pressed: '{}'", input);
} else {
log::debug!("Non-character keysym pressed: {:?}", event.keysym);
}
// Convert keysym to character
let ch = self.keysym_to_char(keysym);
if let Ok(mut lock_manager) = self.lock_manager.lock() {
if let Some(ch) = ch {
log::info!("Key pressed: '{}'", ch);
// 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 {
log::debug!("Non-character keysym pressed");
}
}
}
fn keysym_to_char(
&self,
_keysym: smithay_client_toolkit::seat::keyboard::Keysym,
) -> Option<char> {
None
}
}
@@ -458,6 +487,11 @@ fn lock_wayland_session(
seat_state: SeatState::new(&globals, &qh),
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,
lock_surfaces: Vec::new(),
lock_manager,
@@ -521,24 +555,10 @@ fn run_demonstration_mode(
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_clone = running.clone();
ctrlc::set_handler(move || {
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();
}
}
std::thread::sleep(Duration::from_secs(30));
log::info!("Exiting demonstration mode");
Ok(())