From 79be8d298843e5dee1b37f534238474fc5d3c103 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 1 Aug 2021 15:44:09 +0200 Subject: [PATCH] feat(core): implement first draft of secure input workaround script --- espanso/src/cli/mod.rs | 1 + espanso/src/cli/workaround/mod.rs | 53 +++++++ espanso/src/cli/workaround/secure_input.rs | 140 ++++++++++++++++++ espanso/src/cli/worker/secure_input.rs | 2 +- espanso/src/exit_code.rs | 4 + espanso/src/main.rs | 15 +- .../macos/scripts/blur_chrome_windows.scpt | 15 ++ .../res/macos/scripts/focus_bitwarden.scpt | 4 + .../res/macos/scripts/get_running_apps.scpt | 5 + .../scripts/secure_input_ask_lock_screen.scpt | 8 + .../scripts/secure_input_disabled_dialog.scpt | 1 + 11 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 espanso/src/cli/workaround/mod.rs create mode 100644 espanso/src/cli/workaround/secure_input.rs create mode 100644 espanso/src/res/macos/scripts/blur_chrome_windows.scpt create mode 100644 espanso/src/res/macos/scripts/focus_bitwarden.scpt create mode 100644 espanso/src/res/macos/scripts/get_running_apps.scpt create mode 100644 espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt create mode 100644 espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs index 956595c..b792645 100644 --- a/espanso/src/cli/mod.rs +++ b/espanso/src/cli/mod.rs @@ -32,6 +32,7 @@ pub mod modulo; pub mod path; pub mod service; pub mod util; +pub mod workaround; pub mod worker; pub struct CliModule { diff --git a/espanso/src/cli/workaround/mod.rs b/espanso/src/cli/workaround/mod.rs new file mode 100644 index 0000000..193d6be --- /dev/null +++ b/espanso/src/cli/workaround/mod.rs @@ -0,0 +1,53 @@ +/* + * 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 super::{CliModule, CliModuleArgs}; +use crate::{error_eprintln, exit_code::{WORKAROUND_FAILURE, WORKAROUND_SUCCESS}}; + +#[cfg(target_os = "macos")] +mod secure_input; + +pub fn new() -> CliModule { + CliModule { + subcommand: "workaround".to_string(), + entry: workaround_main, + ..Default::default() + } +} + +fn workaround_main(args: CliModuleArgs) -> i32 { + let cli_args = args.cli_args.expect("missing cli_args"); + + if cli_args.subcommand_matches("secure-input").is_some() { + #[cfg(target_os = "macos")] + { + if let Err(err) = secure_input::run_secure_input_workaround() { + error_eprintln!("secure-input workaround reported error: {}", err); + return WORKAROUND_FAILURE; + } + } + #[cfg(not(target_os = "macos"))] + { + error_eprintln!("secure-input workaround is only available on macOS"); + return crate::exit_code::WORKAROUND_NOT_AVAILABLE; + } + } + + WORKAROUND_SUCCESS +} diff --git a/espanso/src/cli/workaround/secure_input.rs b/espanso/src/cli/workaround/secure_input.rs new file mode 100644 index 0000000..3addc79 --- /dev/null +++ b/espanso/src/cli/workaround/secure_input.rs @@ -0,0 +1,140 @@ +/* + * 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::{bail, Result}; +use std::io::Write; +use std::{ + collections::HashSet, + process::{Command, Stdio}, +}; + +const BLUR_CHROME_WINDOWS_SCRIPT: &'static str = + include_str!("../../res/macos/scripts/blur_chrome_windows.scpt"); + +const GET_RUNNING_APPS_SCRIPT: &'static str = + include_str!("../../res/macos/scripts/get_running_apps.scpt"); + +const FOCUS_BITWARDEN_SCRIPT: &'static str = + include_str!("../../res/macos/scripts/focus_bitwarden.scpt"); + +const SECURE_INPUT_ASK_LOCK_SCREEN_SCRIPT: &'static str = + include_str!("../../res/macos/scripts/secure_input_ask_lock_screen.scpt"); + +const SUCCESS_DIALOG_SCRIPT: &'static str = + include_str!("../../res/macos/scripts/secure_input_disabled_dialog.scpt"); + +pub fn run_secure_input_workaround() -> Result<()> { + if espanso_mac_utils::get_secure_input_pid().is_none() { + println!("secure input is not active, no workaround needed"); + return Ok(()); + } + + execute_secure_input_workaround()?; + let _ = run_apple_script(SUCCESS_DIALOG_SCRIPT); + Ok(()) +} + +fn execute_secure_input_workaround() -> Result<()> { + println!( + "Secure input is enabled. Our guess is that it was activated by '{}',", + espanso_mac_utils::get_secure_input_application() + .map(|entry| entry.0) + .unwrap_or_default() + ); + println!("so restarting that application could solve the problem."); + println!(""); + println!("Unfortunately, that guess might be wrong if SecureInput was activated by"); + println!("the application while in the background."); + println!(""); + println!("This workaround will attempt to execute a series of known actions that often"); + println!("help in disabling secure input."); + + let running_apps = get_running_apps()?; + + if running_apps.contains("com.google.Chrome") { + println!("-> Running chrome defocusing workaround"); + if let Err(err) = run_apple_script(BLUR_CHROME_WINDOWS_SCRIPT) { + eprintln!("unable to run chrome defocusing workaround: {}", err); + } + + if espanso_mac_utils::get_secure_input_pid().is_none() { + return Ok(()); + } + } + + if running_apps.contains("com.bitwarden.desktop") { + println!("-> Focusing/Defocusing on Bitwarden"); + if let Err(err) = run_apple_script(FOCUS_BITWARDEN_SCRIPT) { + eprintln!("unable to run bitwarden defocusing workaround: {}", err); + } + + if espanso_mac_utils::get_secure_input_pid().is_none() { + return Ok(()); + } + } + + // Ask the user if he wants to try locking the screen + if run_apple_script(SECURE_INPUT_ASK_LOCK_SCREEN_SCRIPT)?.trim() == "yes" { + if let Err(err) = lock_screen() { + eprintln!("failed to lock the screen: {}", err); + } + } + + if espanso_mac_utils::get_secure_input_pid().is_some() { + bail!("failed to release secure input"); + } + + Ok(()) +} + +fn run_apple_script(script: &str) -> Result { + let mut child = Command::new("osascript") + .arg("-") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + let child_stdin = child.stdin.as_mut().unwrap(); + child_stdin.write_all(script.as_bytes())?; + drop(child_stdin); + + let output = child.wait_with_output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + Ok(stdout.to_string()) +} + +fn lock_screen() -> Result<()> { + let mut child = Command::new("osascript") + .arg("-e") + .arg(r#"tell application "System Events" to keystroke "q" using {command down,control down}"#) + .spawn()?; + + child.wait()?; + Ok(()) +} + +fn get_running_apps() -> Result> { + let apps_raw = run_apple_script(GET_RUNNING_APPS_SCRIPT)?; + let mut apps = HashSet::new(); + for app in apps_raw.trim().split(", ") { + apps.insert(app.to_string()); + } + + Ok(apps) +} diff --git a/espanso/src/cli/worker/secure_input.rs b/espanso/src/cli/worker/secure_input.rs index 242db85..13aeaf3 100644 --- a/espanso/src/cli/worker/secure_input.rs +++ b/espanso/src/cli/worker/secure_input.rs @@ -82,7 +82,7 @@ fn secure_input_main( 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); + info!("secure input has been acquired, preventing espanso from working correctly. Our guess is that this is caused by '{}', but there are cases in which the detection is unreliable. Full path: {}", app_name, app_path); if let Err(error) = secure_input_sender.send(SecureInputEvent::Enabled { app_name, app_path }) diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs index 26e5125..9ea341c 100644 --- a/espanso/src/exit_code.rs +++ b/espanso/src/exit_code.rs @@ -51,6 +51,10 @@ pub const SERVICE_NOT_REGISTERED: i32 = 2; pub const SERVICE_ALREADY_RUNNING: i32 = 3; pub const SERVICE_NOT_RUNNING: i32 = 4; +pub const WORKAROUND_SUCCESS: i32 = 0; +pub const WORKAROUND_FAILURE: i32 = 1; +pub const WORKAROUND_NOT_AVAILABLE: i32 = 2; + use std::sync::Mutex; lazy_static! { diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 32f2cd2..11dce8e 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -68,8 +68,8 @@ lazy_static! { cli::migrate::new(), cli::env_path::new(), cli::service::new(), + cli::workaround::new(), ]; - static ref ALIASES: Vec = vec![ CliAlias { subcommand: "start".to_owned(), @@ -345,6 +345,14 @@ fn main() { // .subcommand(SubCommand::with_name("refresh") // .about("Update espanso package index")) // ) + .subcommand( + SubCommand::with_name("workaround") + .subcommand( + SubCommand::with_name("secure-input") + .about("Attempt to disable secure input by automating the common steps."), + ) + .about("A collection of workarounds to solve some common problems."), + ) .subcommand( SubCommand::with_name("worker") .setting(AppSettings::Hidden) @@ -388,10 +396,10 @@ fn main() { _ => LevelFilter::Debug, }; - let alias = ALIASES + let alias = ALIASES .iter() .find(|cli| matches.subcommand_matches(&cli.subcommand).is_some()); - + let mut handler = if let Some(alias) = alias { CLI_HANDLERS .iter() @@ -402,7 +410,6 @@ fn main() { .find(|cli| matches.subcommand_matches(&cli.subcommand).is_some()) }; - // When started from the macOS App Bundle, override the default // handler with "launcher" if not present, otherwise the GUI could not be started. if handler.is_none() { diff --git a/espanso/src/res/macos/scripts/blur_chrome_windows.scpt b/espanso/src/res/macos/scripts/blur_chrome_windows.scpt new file mode 100644 index 0000000..f45cb54 --- /dev/null +++ b/espanso/src/res/macos/scripts/blur_chrome_windows.scpt @@ -0,0 +1,15 @@ +-- Activate each Window of Google Chrome and press CMD+L to focus the address bar +-- This makes it possible to "blur" any focused password field that might be keeping +-- SecureInput enabled +tell application "Google Chrome" + activate + -- For each window + repeat with win in windows + -- Bring to front + set index of item 1 of win to 1 + delay 0.5 + -- And press CMD+L + tell application "System Events" to keystroke "l" using command down + delay 0.5 + end repeat +end tell \ No newline at end of file diff --git a/espanso/src/res/macos/scripts/focus_bitwarden.scpt b/espanso/src/res/macos/scripts/focus_bitwarden.scpt new file mode 100644 index 0000000..37723cd --- /dev/null +++ b/espanso/src/res/macos/scripts/focus_bitwarden.scpt @@ -0,0 +1,4 @@ +tell application "Bitwarden" to activate +delay 1 +tell application "Finder" to activate +delay 1 \ No newline at end of file diff --git a/espanso/src/res/macos/scripts/get_running_apps.scpt b/espanso/src/res/macos/scripts/get_running_apps.scpt new file mode 100644 index 0000000..8a1185f --- /dev/null +++ b/espanso/src/res/macos/scripts/get_running_apps.scpt @@ -0,0 +1,5 @@ +tell application "System Events" + set listOfProcesses to (bundle identifier of every process where background only is false) +end tell + +return listOfProcesses \ No newline at end of file diff --git a/espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt b/espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt new file mode 100644 index 0000000..b33681f --- /dev/null +++ b/espanso/src/res/macos/scripts/secure_input_ask_lock_screen.scpt @@ -0,0 +1,8 @@ +display alert "Espanso wasn't able to automatically disable secure input. Sometimes locking and unlocking the screen helps, do you want to try?" buttons {"No", "Yes"} default button "Yes" +if button returned of result = "No" then + return "no" +else + if button returned of result = "Yes" then + return "yes" + end if +end if \ No newline at end of file diff --git a/espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt b/espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt new file mode 100644 index 0000000..adc40c6 --- /dev/null +++ b/espanso/src/res/macos/scripts/secure_input_disabled_dialog.scpt @@ -0,0 +1 @@ +display alert "Secure input successfully disabled!" buttons {"Great!"} \ No newline at end of file