Removed control+c behaviour and updated keyboard handler and pam auth
This commit is contained in:
Generated
+349
-233
File diff suppressed because it is too large
Load Diff
+6
-7
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user