diff --git a/Cargo.lock b/Cargo.lock index 41bab16..60799b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "caps" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c088f2dddef283f86b023ab1ebe2301c653326834996458b2f48d29b804e9540" +dependencies = [ + "errno", + "libc", + "thiserror", +] + [[package]] name = "cc" version = "1.0.66" @@ -412,11 +423,33 @@ dependencies = [ "syn 1.0.67", ] +[[package]] +name = "errno" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067" +dependencies = [ + "gcc", + "libc", +] + [[package]] name = "espanso" version = "2.0.0" dependencies = [ "anyhow", + "caps", "clap", "colored", "crossbeam", @@ -774,6 +807,12 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "getrandom" version = "0.1.16" diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 30d123a..751243b 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -66,4 +66,7 @@ widestring = "0.4.3" libc = "0.2.98" [target.'cfg(target_os="macos")'.dependencies] -espanso-mac-utils = { path = "../espanso-mac-utils" } \ No newline at end of file +espanso-mac-utils = { path = "../espanso-mac-utils" } + +[target.'cfg(target_os="linux")'.dependencies] +caps = "0.5.2" \ No newline at end of file diff --git a/espanso/src/capabilities/fallback.rs b/espanso/src/capabilities/fallback.rs new file mode 100644 index 0000000..10b08c9 --- /dev/null +++ b/espanso/src/capabilities/fallback.rs @@ -0,0 +1,32 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use anyhow::Result; + +pub fn can_use_capabilities() -> bool { + false +} + +pub fn grant_capabilities() -> Result<()> { + Ok(()) +} + +pub fn clear_capabilities() -> Result<()> { + Ok(()) +} diff --git a/espanso/src/capabilities/linux.rs b/espanso/src/capabilities/linux.rs new file mode 100644 index 0000000..8a7a1db --- /dev/null +++ b/espanso/src/capabilities/linux.rs @@ -0,0 +1,42 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ +use anyhow::Result; +use caps::{CapSet, Capability}; +use log::error; + +pub fn can_use_capabilities() -> bool { + match caps::has_cap(None, CapSet::Permitted, Capability::CAP_DAC_OVERRIDE) { + Ok(has_cap) => has_cap, + Err(err) => { + error!("error while checking if capabilities are enabled: {}", err); + false + }, + } +} + +pub fn grant_capabilities() -> Result<()> { + caps::raise(None, CapSet::Effective, Capability::CAP_DAC_OVERRIDE)?; + Ok(()) +} + +pub fn clear_capabilities() -> Result<()> { + caps::clear(None, CapSet::Effective)?; + caps::clear(None, CapSet::Permitted)?; + Ok(()) +} \ No newline at end of file diff --git a/espanso/src/capabilities/mod.rs b/espanso/src/capabilities/mod.rs new file mode 100644 index 0000000..5204961 --- /dev/null +++ b/espanso/src/capabilities/mod.rs @@ -0,0 +1,29 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use linux::*; + +#[cfg(not(target_os = "linux"))] +mod fallback; +#[cfg(not(target_os = "linux"))] +pub use fallback::*; + diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs index a0ce61d..7ac0e32 100644 --- a/espanso/src/cli/mod.rs +++ b/espanso/src/cli/mod.rs @@ -42,6 +42,7 @@ pub struct CliModule { pub requires_config: bool, pub subcommand: String, pub show_in_dock: bool, + pub requires_linux_capabilities: bool, pub entry: fn(CliModuleArgs)->i32, } @@ -55,6 +56,7 @@ impl Default for CliModule { requires_config: false, subcommand: "".to_string(), show_in_dock: false, + requires_linux_capabilities: false, entry: |_| {0}, } } diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs index f912c79..2276a30 100644 --- a/espanso/src/cli/worker/engine/funnel/mod.rs +++ b/espanso/src/cli/worker/engine/funnel/mod.rs @@ -18,7 +18,7 @@ */ use anyhow::Result; -use espanso_detect::event::{InputEvent, KeyboardEvent, Status}; +use espanso_detect::{SourceCreationOptions, event::{InputEvent, KeyboardEvent, Status}}; use log::{error}; use thiserror::Error; @@ -33,8 +33,7 @@ pub mod secure_input; pub mod sequencer; pub mod ui; -// TODO: pass options -pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)> { +pub fn init_and_spawn(source_options: SourceCreationOptions) -> Result<(DetectSource, ModifierStateStore, Sequencer)> { let (sender, receiver) = crossbeam::channel::unbounded(); let (init_tx, init_rx) = crossbeam::channel::unbounded(); @@ -46,7 +45,7 @@ pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)> if let Err(error) = std::thread::Builder::new() .name("detect thread".to_string()) .spawn( - move || match espanso_detect::get_source(Default::default()) { + move || match espanso_detect::get_source(source_options) { Ok(mut source) => { if source.initialize().is_err() { init_tx diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index fa5fd1e..294ead4 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -22,18 +22,39 @@ use std::thread::JoinHandle; use anyhow::Result; use crossbeam::channel::Receiver; use espanso_config::{config::ConfigStore, matches::store::MatchStore}; +use espanso_detect::SourceCreationOptions; +use espanso_inject::InjectorCreationOptions; use espanso_path::Paths; use espanso_ui::{event::UIEvent, UIRemote}; -use log::info; +use log::{debug, error, info, warn}; -use crate::{cli::worker::{engine::{dispatch::executor::{ +use crate::{ + cli::worker::{ + engine::{ + dispatch::executor::{ clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter, event_injector::EventInjectorAdapter, icon::IconHandlerAdapter, key_injector::KeyInjectorAdapter, - }, process::middleware::{image_resolve::PathProviderAdapter, match_select::MatchSelectorAdapter, matcher::{convert::MatchConverter, regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions}, rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions}}, multiplex::MultiplexAdapter, render::{ + }, + process::middleware::{ + image_resolve::PathProviderAdapter, + match_select::MatchSelectorAdapter, + matcher::{ + convert::MatchConverter, + regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions}, + rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions}, + }, + multiplex::MultiplexAdapter, + render::{ extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter}, RendererAdapter, - }}}, match_cache::MatchCache}, engine::event::ExitMode}; + }, + }, + }, + match_cache::MatchCache, + }, + engine::event::ExitMode, +}; use super::secure_input::SecureInputEvent; @@ -41,6 +62,7 @@ pub mod dispatch; pub mod funnel; pub mod process; +#[allow(clippy::too_many_arguments)] pub fn initialize_and_spawn( paths: Paths, config_store: Box, @@ -49,6 +71,7 @@ pub fn initialize_and_spawn( exit_signal: Receiver, ui_event_receiver: Receiver, secure_input_receiver: Receiver, + use_evdev_backend: bool, ) -> Result> { let handle = std::thread::Builder::new() .name("engine thread".to_string()) @@ -66,18 +89,35 @@ pub fn initialize_and_spawn( let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager); let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager); + let has_granted_capabilities = grant_linux_capabilities(use_evdev_backend); + + // TODO: pass all the options let (detect_source, modifier_state_store, sequencer) = - super::engine::funnel::init_and_spawn().expect("failed to initialize detector module"); + super::engine::funnel::init_and_spawn(SourceCreationOptions { + use_evdev: use_evdev_backend, + ..Default::default() + }) + .expect("failed to initialize detector module"); let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer); let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer); - let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(secure_input_receiver, &sequencer); - let sources: Vec<&dyn crate::engine::funnel::Source> = - vec![&detect_source, &exit_source, &ui_source, &secure_input_source]; + let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new( + secure_input_receiver, + &sequencer, + ); + let sources: Vec<&dyn crate::engine::funnel::Source> = vec![ + &detect_source, + &exit_source, + &ui_source, + &secure_input_source, + ]; let funnel = crate::engine::funnel::default(&sources); - let rolling_matcher = RollingMatcherAdapter::new(&match_converter.get_rolling_matches(), RollingMatcherAdapterOptions { - char_word_separators: config_manager.default().word_separators(), - }); + let rolling_matcher = RollingMatcherAdapter::new( + &match_converter.get_rolling_matches(), + RollingMatcherAdapterOptions { + char_word_separators: config_manager.default().word_separators(), + }, + ); let regex_matcher = RegexMatcherAdapter::new( &match_converter.get_regex_matches(), &RegexMatcherAdapterOptions { @@ -92,8 +132,11 @@ pub fn initialize_and_spawn( let selector = MatchSelectorAdapter::new(&modulo_search_ui, &match_cache); let multiplexer = MultiplexAdapter::new(&match_cache); - let injector = espanso_inject::get_injector(Default::default()) - .expect("failed to initialize injector module"); // TODO: handle the options + let injector = espanso_inject::get_injector(InjectorCreationOptions { + use_evdev: use_evdev_backend, + ..Default::default() + }) + .expect("failed to initialize injector module"); // TODO: handle the options let clipboard = espanso_clipboard::get_clipboard(Default::default()) .expect("failed to initialize clipboard module"); // TODO: handle options @@ -161,6 +204,13 @@ pub fn initialize_and_spawn( &icon_adapter, ); + // Disable previously granted linux capabilities if not needed anymore + if has_granted_capabilities { + if let Err(err) = crate::capabilities::clear_capabilities() { + error!("unable to revoke linux capabilities: {}", err); + } + } + let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher); let exit_mode = engine.run(); @@ -172,3 +222,30 @@ pub fn initialize_and_spawn( Ok(handle) } + +fn grant_linux_capabilities(use_evdev_backend: bool) -> bool { + if use_evdev_backend { + if crate::capabilities::can_use_capabilities() { + debug!("using linux capabilities to grant permissions needed by EVDEV backend"); + if let Err(err) = crate::capabilities::grant_capabilities() { + error!("unable to grant CAP_DAC_OVERRIDE capability: {}", err); + false + } else { + debug!("successfully granted permissions using capabilities"); + true + } + } else { + warn!("EVDEV backend is being used, but without enabling linux capabilities."); + warn!(" Although you CAN run espanso EVDEV backend as root, it's not recommended due"); + warn!( + " to security reasons. Espanso supports linux capabilities to limit the attack surface" + ); + warn!(" area by only leveraging on the CAP_DAC_OVERRIDE capability (needed to work with"); + warn!(" /dev/input/* devices to detect and inject text) and disabling it as soon as the"); + warn!(" initial setup is completed."); + false + } + } else { + false + } +} diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs index f56bb24..b411638 100644 --- a/espanso/src/cli/worker/mod.rs +++ b/espanso/src/cli/worker/mod.rs @@ -46,6 +46,7 @@ pub fn new() -> CliModule { CliModule { requires_paths: true, requires_config: true, + requires_linux_capabilities: true, enable_logs: true, log_mode: super::LogMode::AppendOnly, subcommand: "worker".to_string(), @@ -81,6 +82,12 @@ fn worker_main(args: CliModuleArgs) -> i32 { // TODO: show config loading errors in a GUI, if any + let use_evdev_backend = if cfg!(feature = "wayland") { + true + } else { + std::env::var("USE_EVDEV").unwrap_or_else(|_| "false".to_string()) == "true" + }; + let icon_paths = crate::icon::load_icon_paths(&paths.runtime).expect("unable to initialize icons"); @@ -112,6 +119,7 @@ fn worker_main(args: CliModuleArgs) -> i32 { engine_exit_receiver, engine_ui_event_receiver, engine_secure_input_receiver, + use_evdev_backend, ) .expect("unable to initialize engine"); diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 93d28c4..eda2df2 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -35,6 +35,7 @@ use simplelog::{ use crate::cli::{LogMode, PathsOverrides}; +mod capabilities; mod cli; mod config; mod engine; @@ -401,6 +402,13 @@ fn main() { log_panics::init(); } + // If the process doesn't require linux capabilities, disable them + if !handler.requires_linux_capabilities { + if let Err(err) = crate::capabilities::clear_capabilities() { + error!("unable to clear linux capabilities: {}", err); + } + } + // If explicitly requested, we show the Dock icon on macOS // We need to enable this selectively, otherwise we would end up with multiple // dock icons due to the multi-process nature of espanso.