From 09095a1e8d8443134fe5f41f2a72ba6020353c0d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 9 Jun 2021 21:49:27 +0200 Subject: [PATCH] feat(core): implement secure-input watcher --- Cargo.lock | 14 +++ Cargo.toml | 1 + espanso/Cargo.toml | 5 +- espanso/src/cli/worker/engine/funnel/mod.rs | 1 + .../cli/worker/engine/funnel/secure_input.rs | 66 +++++++++++ espanso/src/cli/worker/engine/mod.rs | 6 +- espanso/src/cli/worker/mod.rs | 6 + espanso/src/cli/worker/secure_input.rs | 107 ++++++++++++++++++ espanso/src/engine/event/internal.rs | 6 + espanso/src/engine/event/mod.rs | 2 +- .../engine/process/middleware/icon_status.rs | 2 +- 11 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 espanso/src/cli/worker/engine/funnel/secure_input.rs create mode 100644 espanso/src/cli/worker/secure_input.rs diff --git a/Cargo.lock b/Cargo.lock index 28c5866..a6ff414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,7 @@ dependencies = [ "espanso-info", "espanso-inject", "espanso-ipc", + "espanso-mac-utils", "espanso-match", "espanso-migrate", "espanso-modulo", @@ -548,6 +549,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "espanso-mac-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "cc", + "lazy_static", + "lazycell", + "log", + "regex", + "thiserror", +] + [[package]] name = "espanso-match" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c668f9b..f3a1639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ members = [ "espanso-path", "espanso-modulo", "espanso-migrate", + "espanso-mac-utils", ] \ No newline at end of file diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 763e545..a12194c 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -57,4 +57,7 @@ notify = "4.0.17" [target.'cfg(windows)'.dependencies] named_pipe = "0.4.1" -winapi = { version = "0.3.9", features = ["wincon"] } \ No newline at end of file +winapi = { version = "0.3.9", features = ["wincon"] } + +[target.'cfg(target_os="macos")'.dependencies] +espanso-mac-utils = { path = "../espanso-mac-utils" } \ No newline at end of file diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs index 29a5224..f912c79 100644 --- a/espanso/src/cli/worker/engine/funnel/mod.rs +++ b/espanso/src/cli/worker/engine/funnel/mod.rs @@ -29,6 +29,7 @@ use self::{modifier::{Modifier, ModifierStateStore}, sequencer::Sequencer}; pub mod detect; pub mod exit; pub mod modifier; +pub mod secure_input; pub mod sequencer; pub mod ui; diff --git a/espanso/src/cli/worker/engine/funnel/secure_input.rs b/espanso/src/cli/worker/engine/funnel/secure_input.rs new file mode 100644 index 0000000..bc68474 --- /dev/null +++ b/espanso/src/cli/worker/engine/funnel/secure_input.rs @@ -0,0 +1,66 @@ +/* + * 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 crossbeam::channel::{Receiver, Select, SelectedOperation}; + +use crate::{ + cli::worker::secure_input::SecureInputEvent, + engine::{ + event::{internal::SecureInputEnabledEvent, Event, EventType}, + funnel, + }, +}; + +use super::sequencer::Sequencer; + +pub struct SecureInputSource<'a> { + pub receiver: Receiver, + pub sequencer: &'a Sequencer, +} + +impl<'a> SecureInputSource<'a> { + pub fn new(receiver: Receiver, sequencer: &'a Sequencer) -> Self { + SecureInputSource { + receiver, + sequencer, + } + } +} + +impl<'a> funnel::Source<'a> for SecureInputSource<'a> { + fn register(&'a self, select: &mut Select<'a>) -> usize { + select.recv(&self.receiver) + } + + fn receive(&self, op: SelectedOperation) -> Event { + let si_event = op + .recv(&self.receiver) + .expect("unable to select data from SecureInputSource receiver"); + + Event { + source_id: self.sequencer.next_id(), + etype: match si_event { + SecureInputEvent::Disabled => EventType::SecureInputDisabled, + SecureInputEvent::Enabled { app_name, app_path } => { + EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path }) + } + }, + } + } +} diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 9b3c62b..a7ddcef 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -54,6 +54,8 @@ use crate::{ engine::event::ExitMode, }; +use super::secure_input::SecureInputEvent; + pub mod dispatch; pub mod funnel; pub mod process; @@ -65,6 +67,7 @@ pub fn initialize_and_spawn( ui_remote: Box, exit_signal: Receiver<()>, ui_event_receiver: Receiver, + secure_input_receiver: Receiver, ) -> Result> { let handle = std::thread::Builder::new() .name("engine thread".to_string()) @@ -86,8 +89,9 @@ pub fn initialize_and_spawn( super::engine::funnel::init_and_spawn().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]; + 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()); diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs index bcc5914..f56bb24 100644 --- a/espanso/src/cli/worker/mod.rs +++ b/espanso/src/cli/worker/mod.rs @@ -38,6 +38,7 @@ mod daemon_monitor; mod engine; mod ipc; mod match_cache; +mod secure_input; mod ui; pub fn new() -> CliModule { @@ -100,6 +101,7 @@ fn worker_main(args: CliModuleArgs) -> i32 { let (engine_exit_notify, engine_exit_receiver) = unbounded(); let (engine_ui_event_sender, engine_ui_event_receiver) = unbounded(); + let (engine_secure_input_sender, engine_secure_input_receiver) = unbounded(); // Initialize the engine on another thread and start it let engine_handle = engine::initialize_and_spawn( @@ -109,6 +111,7 @@ fn worker_main(args: CliModuleArgs) -> i32 { remote, engine_exit_receiver, engine_ui_event_receiver, + engine_secure_input_receiver, ) .expect("unable to initialize engine"); @@ -125,6 +128,9 @@ fn worker_main(args: CliModuleArgs) -> i32 { .expect("unable to initialize daemon monitor thread"); } + secure_input::initialize_and_spawn(engine_secure_input_sender) + .expect("unable to initialize secure input watcher"); + eventloop .run(Box::new(move |event| { if let Err(error) = engine_ui_event_sender.send(event) { diff --git a/espanso/src/cli/worker/secure_input.rs b/espanso/src/cli/worker/secure_input.rs new file mode 100644 index 0000000..469afa4 --- /dev/null +++ b/espanso/src/cli/worker/secure_input.rs @@ -0,0 +1,107 @@ +/* + * 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 std::time::Duration; + +use anyhow::Result; +use crossbeam::channel::Sender; +use log::{error, info}; + +pub enum SecureInputEvent { + Disabled, + Enabled { app_name: String, app_path: String }, +} + +#[cfg(not(target_os = "macos"))] +pub fn initialize_and_spawn(_secure_input_send: Sender) -> Result<()> { + // NOOP on Windows and Linux + Ok(()) +} + +#[cfg(target_os = "macos")] +pub fn initialize_and_spawn(secure_input_sender: Sender) -> Result<()> { + std::thread::Builder::new() + .name("secure-input-monitor".to_string()) + .spawn(move || { + // TODO: pass interval from config parameter + secure_input_main(secure_input_sender, std::time::Duration::from_secs(5)); + })?; + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn secure_input_main(secure_input_sender: Sender, watch_interval: Duration) { + info!("monitoring the status of secure input"); + + let mut last_secure_input_pid: Option = None; + loop { + let pid = espanso_mac_utils::get_secure_input_pid(); + + if let Some(pid) = pid { + // Some application is currently on SecureInput + let should_notify = if let Some(old_pid) = last_secure_input_pid { + // We already detected a SecureInput app + if old_pid != pid { + // The old app is different from the current one, we should take action + true + } else { + // We already notified this application before + false + } + } else { + // First time we see this SecureInput app, we should take action + true + }; + + if should_notify { + let secure_input_app = espanso_mac_utils::get_secure_input_application(); + + if let Some((app_name, app_path)) = secure_input_app { + info!("secure input has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, app_path); + + if let Err(error) = + secure_input_sender.send(SecureInputEvent::Enabled { app_name, app_path }) + { + error!("unable to send secure input disabled event: {}", error); + } + } else { + error!("detected secure input, but could not figure out which application triggered it"); + } + } + + last_secure_input_pid = Some(pid); + } else { + // No app is currently keeping SecureInput + + // If there was an app with SecureInput, notify that is now free + if let Some(old_pid) = last_secure_input_pid { + info!("secure input has been disabled"); + + if let Err(error) = secure_input_sender.send(SecureInputEvent::Disabled) { + error!("unable to send secure input disabled event: {}", error); + } + } + + last_secure_input_pid = None + } + + std::thread::sleep(watch_interval); + } +} diff --git a/espanso/src/engine/event/internal.rs b/espanso/src/engine/event/internal.rs index 1afed3c..642a0b1 100644 --- a/espanso/src/engine/event/internal.rs +++ b/espanso/src/engine/event/internal.rs @@ -82,4 +82,10 @@ pub struct RenderedEvent { pub struct DiscardPreviousEvent { // All Events with a source_id smaller than this one will be discarded pub minimum_source_id: u32, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SecureInputEnabledEvent { + pub app_name: String, + pub app_path: String, } \ No newline at end of file diff --git a/espanso/src/engine/event/mod.rs b/espanso/src/engine/event/mod.rs index 89b843f..029676c 100644 --- a/espanso/src/engine/event/mod.rs +++ b/espanso/src/engine/event/mod.rs @@ -74,7 +74,7 @@ pub enum EventType { Enabled, DisableRequest, EnableRequest, - SecureInputEnabled, + SecureInputEnabled(internal::SecureInputEnabledEvent), SecureInputDisabled, // Effects diff --git a/espanso/src/engine/process/middleware/icon_status.rs b/espanso/src/engine/process/middleware/icon_status.rs index 1bec98b..67ae4ba 100644 --- a/espanso/src/engine/process/middleware/icon_status.rs +++ b/espanso/src/engine/process/middleware/icon_status.rs @@ -51,7 +51,7 @@ impl Middleware for IconStatusMiddleware { match &event.etype { EventType::Enabled => *enabled = true, EventType::Disabled => *enabled = false, - EventType::SecureInputEnabled => *secure_input_enabled = true, + EventType::SecureInputEnabled(_) => *secure_input_enabled = true, EventType::SecureInputDisabled => *secure_input_enabled = false, _ => did_update = false, }