diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..04ae3ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "raylock" +version = "0.1.0" +edition = "2021" + +[dependencies] +eframe = "0.29.1" +egui = "0.29.1" diff --git a/README.md b/README.md index 37f5bdd..f7722d7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # raylock - Swaylock alternitive made in rust +##### Swaylock alternitive made in rust +--- +Unfortunatly this is not the most secure desktop locker, as it involves using sway config, and not PAM for key validiation. But it seems to work just fine. + +``` +# Add this to your sway config: +mode "lock" { } +for_window [title="^raylock$"] sticky enable, fullscreen +``` diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..cadaf8f --- /dev/null +++ b/src/input.rs @@ -0,0 +1,235 @@ +use egui::Key; +use std::process::Command; +use std::thread; + +pub fn sway_lock_input() { + thread::spawn(move || { + let _ = Command::new("swaymsg").args(["mode", "lock"]).spawn(); + }); +} + +pub fn sway_unlock_input() { + thread::spawn(move || { + let _ = Command::new("swaymsg").args(["mode", "default"]).spawn(); + }); +} + +pub fn format_key(key: Key, shift_pressed: bool) -> String { + match key { + // Letters + Key::A + | Key::B + | Key::C + | Key::D + | Key::E + | Key::F + | Key::G + | Key::H + | Key::I + | Key::J + | Key::K + | Key::L + | Key::M + | Key::N + | Key::O + | Key::P + | Key::Q + | Key::R + | Key::S + | Key::T + | Key::U + | Key::V + | Key::W + | Key::X + | Key::Y + | Key::Z => { + let base_char = (key as u8 - Key::A as u8 + b'a') as char; + if shift_pressed { + base_char.to_uppercase().to_string() + } else { + base_char.to_string() + } + } + + // Numbers and their shift symbols + Key::Num0 => { + if shift_pressed { + ")".to_string() + } else { + "0".to_string() + } + } + Key::Num1 => { + if shift_pressed { + "!".to_string() + } else { + "1".to_string() + } + } + Key::Num2 => { + if shift_pressed { + "@".to_string() + } else { + "2".to_string() + } + } + Key::Num3 => { + if shift_pressed { + "#".to_string() + } else { + "3".to_string() + } + } + Key::Num4 => { + if shift_pressed { + "$".to_string() + } else { + "4".to_string() + } + } + Key::Num5 => { + if shift_pressed { + "%".to_string() + } else { + "5".to_string() + } + } + Key::Num6 => { + if shift_pressed { + "^".to_string() + } else { + "6".to_string() + } + } + Key::Num7 => { + if shift_pressed { + "&".to_string() + } else { + "7".to_string() + } + } + Key::Num8 => { + if shift_pressed { + "*".to_string() + } else { + "8".to_string() + } + } + Key::Num9 => { + if shift_pressed { + "(".to_string() + } else { + "9".to_string() + } + } + + // Special characters + Key::Space => " ".to_string(), + // Key::Tab => "Tab".to_string(), + // Key::Enter => "Enter".to_string(), + // Key::Backspace => "Backspace".to_string(), + // Key::Escape => "Esc".to_string(), + // Key::Delete => "Del".to_string(), + + // Arrow keys + // Key::ArrowLeft => "←".to_string(), + // Key::ArrowRight => "→".to_string(), + // Key::ArrowUp => "↑".to_string(), + // Key::ArrowDown => "↓".to_string(), + + // Punctuation and symbols + Key::Minus => { + if shift_pressed { + "_".to_string() + } else { + "-".to_string() + } + } + Key::Equals => { + if shift_pressed { + "+".to_string() + } else { + "=".to_string() + } + } + Key::OpenBracket => { + if shift_pressed { + "{".to_string() + } else { + "[".to_string() + } + } + Key::CloseBracket => { + if shift_pressed { + "}".to_string() + } else { + "]".to_string() + } + } + Key::Backslash => { + if shift_pressed { + "|".to_string() + } else { + "\\".to_string() + } + } + Key::Semicolon => { + if shift_pressed { + ":".to_string() + } else { + ";".to_string() + } + } + Key::Quote => { + if shift_pressed { + "\"".to_string() + } else { + "'".to_string() + } + } + Key::Comma => { + if shift_pressed { + "<".to_string() + } else { + ",".to_string() + } + } + Key::Period => { + if shift_pressed { + ">".to_string() + } else { + ".".to_string() + } + } + Key::Slash => { + if shift_pressed { + "?".to_string() + } else { + "/".to_string() + } + } + Key::Backtick => { + if shift_pressed { + "~".to_string() + } else { + "`".to_string() + } + } + + // Function keys + // Key::F1..=Key::F12 => format!("F{}", (key as u8 - Key::F1 as u8 + 1)), + + // Modifier keys + // Key::Control => "Ctrl".to_string(), + // Key::Alt => "Alt".to_string(), + // Key::Shift => "Shift".to_string(), + // Key::Insert => "Insert".to_string(), + // Key::Home => "Home".to_string(), + // Key::End => "End".to_string(), + // Key::PageUp => "PgUp".to_string(), + // Key::PageDown => "PgDn".to_string(), + + // Catch any other keys + _ => "".to_string(), + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9815c0b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,141 @@ +use eframe::egui; +use egui::Key; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +mod input; +mod structs; +mod ui; + +#[derive(Default)] +struct ExampleApp { + auth_state: Arc>, +} + +impl ExampleApp { + fn name() -> &'static str { + "raylock" + } +} + +impl eframe::App for ExampleApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let mut state = self.auth_state.lock().unwrap(); + ctx.set_pixels_per_point(1.5); + + if ctx.input(|i| i.events.len() > 0) { + ctx.input(|i| { + for event in &i.events { + if let egui::Event::Key { + key, + pressed, + modifiers, + .. + } = event + { + if *pressed { + match key { + Key::Enter => { + state.to_be_submitted = true; + } + Key::Backspace => { + let len = state.password.len(); + if len != 0 { + state.password.remove(len - 1 as usize); + } + } + _ => { + let str = &input::format_key(*key, modifiers.shift); + state.password += str; + } + } + //let mod_str = if modifiers.is_empty() { + // String::new() + //} else { + // format!(" + {:?}", modifiers) + //}; + } + } + } + }); + } + + egui::CentralPanel::default().show(ctx, |ui| { + // ui.add_space(200.0); + // ui.heading("*".repeat(state.password.clone().len())); + // ui.add_space(20.0); + // + ui::update(state, ctx, _frame, ui); + }); + } +} + +fn main() -> eframe::Result<()> { + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size((400.0, 400.0)), + ..eframe::NativeOptions::default() + }; + + let state = Arc::new(Mutex::new(structs::AuthState { + password: String::new(), + to_be_submitted: false, + failed_attempts: 0, + })); + + let auth_state_clone = state.clone(); + + // Spawn authentication thread + thread::spawn(move || loop { + let mut state = auth_state_clone.lock().unwrap(); + + if state.to_be_submitted { + let result = try_sudo(&state.password); + match result { + Ok(true) => { + println!("True"); + input::sway_unlock_input(); + std::process::exit(0); + } + Ok(false) => { + println!("False"); + state.failed_attempts += 1; + state.password.clear(); + } + Err(_) => { + state.password.clear(); + } + } + state.to_be_submitted = false; + } + drop(state); + thread::sleep(Duration::from_millis(100)); + }); + + input::sway_lock_input(); + + eframe::run_native( + ExampleApp::name(), + native_options, + Box::new(|_| Ok(Box::::new(ExampleApp { auth_state: state }))), + ) +} + +fn try_sudo(password: &str) -> Result { + let mut child = Command::new("sudo") + .args(["-kS", "true"]) // Use -S to read password from stdin + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + if let Some(mut stdin) = child.stdin.take() { + use std::io::Write; + writeln!(stdin, "{}", password)?; + } + + match child.wait() { + Ok(status) => Ok(status.success()), + Err(e) => Err(e), + } +} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..88846a5 --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,6 @@ +#[derive(Default)] +pub struct AuthState { + pub password: String, + pub to_be_submitted: bool, + pub failed_attempts: u16, +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..c7ee7b7 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,133 @@ +use crate::structs; +use eframe::egui; +use egui::Color32; +use egui::Pos2; +use egui::Stroke; +use std::f32::consts::PI; + +use std::sync::MutexGuard; + +const TEXT_COLOR: Color32 = Color32::from_rgb(255, 255, 255); +const LOGIN_CIRCLE_RADIUS: f32 = 50.; +const LOGIN_SUBCIRCLE_START_ANG: f32 = -PI / 4.; + +const LOGIN_SUBCIRCLE_RADIUS: f32 = 4.; +const LOGIN_SUBCIRCLE_COLOR: Color32 = Color32::TRANSPARENT; +const LOGIN_SUBCIRCLE_STROKE: Stroke = Stroke { + width: 2.0, + color: Color32::from_rgb(255, 255, 255), +}; +const LOGIN_CIRCLE_LINE_STROKE: Stroke = Stroke { + width: 2.0, + color: TEXT_COLOR, +}; +const LOGIN_FAIL_COLOR: Color32 = Color32::from_rgb(184, 41, 11); +const LOGIN_FAIL_CIRCLE_STROKE: Stroke = Stroke { + width: 5., + color: LOGIN_FAIL_COLOR, +}; +const LOGIN_FAIL_COUNT_CIRCLE_RADIUS: f32 = 15.; +const LOGIN_FAIL_COUNT_CIRCLE_COLOR: Color32 = Color32::TRANSPARENT; +const LOGIN_FAIL_COUNT_CIRCLE_STROKE: Stroke = Stroke { + width: 2., + color: LOGIN_FAIL_COLOR, +}; + +fn rotCircle(i: i16, center: Pos2, rad: f32, offset_ang: f32, ang_per_num: f32) -> Pos2 { + center + + (egui::Vec2 { + x: rad * f32::cos(i as f32 * ang_per_num + offset_ang), + y: rad * f32::sin(i as f32 * ang_per_num + offset_ang), + }) +} + +pub fn update( + wstate: MutexGuard<'_, structs::AuthState>, + ctx: &egui::Context, + frame: &mut eframe::Frame, + ui: &mut egui::Ui, +) { + let mut state = wstate; + let rect = ui.clip_rect(); + let center = Pos2 { + x: rect.width() / 2., + y: rect.height() / 2., + }; + + let painter = ui.painter(); + + // Login Circle + + if state.failed_attempts > 0 { + painter.circle( + center, + LOGIN_CIRCLE_RADIUS - LOGIN_FAIL_CIRCLE_STROKE.width, + Color32::TRANSPARENT, + LOGIN_FAIL_CIRCLE_STROKE, + ); + } + + let ang_per_char = 2. * PI / state.password.len() as f32; + let ang_per_fail = 2. * PI / state.failed_attempts as f32; + let len: i16 = state.password.len() as i16; + + let mut last_pos = rotCircle( + len - 1, + center, + LOGIN_CIRCLE_RADIUS, + LOGIN_SUBCIRCLE_START_ANG, + ang_per_char, + ); + + for i in 0..state.failed_attempts { + let pos: egui::Pos2 = { + if state.failed_attempts <= 1 { + center + } else { + rotCircle( + i as i16, + center, + LOGIN_FAIL_COUNT_CIRCLE_RADIUS, + LOGIN_SUBCIRCLE_START_ANG, + ang_per_fail, + ) + } + }; + + painter.circle( + pos, + LOGIN_SUBCIRCLE_RADIUS, + LOGIN_FAIL_COUNT_CIRCLE_COLOR, + LOGIN_FAIL_COUNT_CIRCLE_STROKE, + ); + } + + for i in 0..len { + let pos: egui::Pos2 = { + if len <= 1 { + center + } else { + rotCircle( + i, + center, + LOGIN_CIRCLE_RADIUS, + LOGIN_SUBCIRCLE_START_ANG, + ang_per_char, + ) + } + }; + + painter.circle( + pos, + LOGIN_SUBCIRCLE_RADIUS, + LOGIN_SUBCIRCLE_COLOR, + LOGIN_SUBCIRCLE_STROKE, + ); + + if len > 1 { + painter.line_segment([last_pos, pos], LOGIN_CIRCLE_LINE_STROKE); + + last_pos = pos; + } + } +}