Add secure input notification on macOS

This commit is contained in:
Federico Terzi 2020-04-03 18:22:31 +02:00
parent 2c8c28087d
commit dd8e5c8f9c
18 changed files with 250 additions and 68 deletions

View File

@ -44,6 +44,7 @@ fn print_config() {
println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=macbridge");
println!("cargo:rustc-link-lib=framework=Cocoa");
println!("cargo:rustc-link-lib=framework=IOKit");
}
fn main()

View File

@ -158,6 +158,15 @@ int32_t set_clipboard(char * text);
*/
int32_t set_clipboard_image(char * path);
/*
* If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
*/
int32_t get_secure_input_process(int64_t *pid);
/*
* Find the executable path corresponding to the given PID, return 0 if no process was found.
*/
int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size);
};
#endif //ESPANSO_BRIDGE_H

View File

@ -20,9 +20,11 @@
#include "bridge.h"
#import <Foundation/Foundation.h>
#include <IOKit/IOKitLib.h>
#include "AppDelegate.h"
#include <stdio.h>
#include <string.h>
#include <libproc.h>
extern "C" {
}
@ -334,3 +336,47 @@ void open_settings_panel() {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
}
// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys
int32_t get_secure_input_process(int64_t *pid) {
NSArray *consoleUsersArray;
io_service_t rootService;
int32_t result = 0;
if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0)
{
if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil)
{
if ([consoleUsersArray isKindOfClass:[NSArray class]]) // Be careful - ensure this really is an array
{
for (NSDictionary *consoleUserDict in consoleUsersArray) {
NSNumber *secureInputPID;
if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil)
{
if ([secureInputPID isKindOfClass:[NSNumber class]])
{
*pid = ((UInt64) [secureInputPID intValue]);
result = 1;
break;
}
}
}
}
CFRelease((CFTypeRef)consoleUsersArray);
}
IOObjectRelease((io_object_t) rootService);
}
return result;
}
int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size) {
int res = proc_pidpath((pid_t) pid, buff, buff_size);
if ( res <= 0 ) {
return 0;
} else {
return 1;
}
}

View File

@ -39,6 +39,8 @@ extern {
pub fn open_settings_panel();
pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32;
pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32;
pub fn get_secure_input_process(pid:*mut i64) -> i32;
pub fn get_path_from_pid(pid:i64, buffer: *mut c_char, size: i32) -> i32;
// Clipboard
pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;

View File

@ -64,6 +64,9 @@ fn default_enable_active() -> bool { true }
fn default_backspace_limit() -> i32 { 3 }
fn default_restore_clipboard_delay() -> i32 { 300 }
fn default_exclude_default_entries() -> bool {false}
fn default_secure_input_watcher_enabled() -> bool {true}
fn default_secure_input_notification() -> bool {true}
fn default_secure_input_watcher_interval() -> i32 {5000}
fn default_matches() -> Vec<Match> { Vec::new() }
fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
@ -138,6 +141,15 @@ pub struct Configs {
#[serde(default = "default_restore_clipboard_delay")]
pub restore_clipboard_delay: i32,
#[serde(default = "default_secure_input_watcher_enabled")]
pub secure_input_watcher_enabled: bool,
#[serde(default = "default_secure_input_watcher_interval")]
pub secure_input_watcher_interval: i32,
#[serde(default = "default_secure_input_notification")]
pub secure_input_notification: bool,
#[serde(default)]
pub backend: BackendType,
@ -190,6 +202,9 @@ impl Configs {
validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
validate_field!(result, self.passive_key, default_passive_key());
validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay());
validate_field!(result, self.secure_input_watcher_enabled, default_secure_input_watcher_enabled());
validate_field!(result, self.secure_input_watcher_interval, default_secure_input_watcher_interval());
validate_field!(result, self.secure_input_notification, default_secure_input_notification());
result
}

View File

@ -29,6 +29,7 @@ use std::{thread, time};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs;
#[repr(C)]
pub struct LinuxContext {
@ -37,7 +38,7 @@ pub struct LinuxContext {
}
impl LinuxContext {
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<LinuxContext> {
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<LinuxContext> {
// Check if the X11 context is available
let x11_available = unsafe {
check_x11()

View File

@ -20,25 +20,30 @@
use std::sync::mpsc::Sender;
use std::os::raw::{c_void, c_char};
use crate::bridge::macos::*;
use crate::event::{Event, KeyEvent, KeyModifier, ActionType};
use crate::event::{Event, KeyEvent, KeyModifier, ActionType, SystemEvent};
use crate::event::KeyModifier::*;
use std::ffi::{CString, CStr};
use std::fs;
use std::{fs, thread};
use log::{info, error, debug};
use std::process::exit;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs;
use std::cell::RefCell;
use crate::system::macos::MacSystemManager;
const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png");
pub struct MacContext {
pub send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>,
secure_input_watcher_enabled: bool,
secure_input_watcher_interval: i32,
}
impl MacContext {
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<MacContext> {
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<MacContext> {
// Check accessibility
unsafe {
let res = prompt_accessibility();
@ -53,7 +58,9 @@ impl MacContext {
let context = Box::new(MacContext {
send_channel,
is_injecting
is_injecting,
secure_input_watcher_enabled: config.secure_input_watcher_enabled,
secure_input_watcher_interval: config.secure_input_watcher_interval,
});
// Initialize the status icon path
@ -81,10 +88,59 @@ impl MacContext {
context
}
fn start_secure_input_watcher(&self) {
let send_channel = self.send_channel.clone();
let secure_input_watcher_interval = self.secure_input_watcher_interval as u64;
let secure_input_watcher = thread::Builder::new().name("secure_input_watcher".to_string()).spawn(move || {
let mut last_secure_input_pid: Option<i64> = None;
loop {
let pid = MacSystemManager::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 = crate::system::macos::MacSystemManager::get_secure_input_application();
if let Some((app_name, path)) = secure_input_app {
let event = Event::System(SystemEvent::SecureInputEnabled(app_name, path));
send_channel.send(event);
}
}
last_secure_input_pid = Some(pid);
}else{ // No app is currently keeping SecureInput
if let Some(old_pid) = last_secure_input_pid { // If there was an app with SecureInput, notify that is now free
let event = Event::System(SystemEvent::SecureInputDisabled);
send_channel.send(event);
}
last_secure_input_pid = None
}
thread::sleep(std::time::Duration::from_millis(secure_input_watcher_interval));
}
});
}
}
impl super::Context for MacContext {
fn eventloop(&self) {
// Start the SecureInput watcher thread
if self.secure_input_watcher_enabled {
self.start_secure_input_watcher();
}
unsafe {
eventloop();
}

View File

@ -32,6 +32,7 @@ use std::path::PathBuf;
use std::fs::create_dir_all;
use std::sync::{Once, Arc};
use std::sync::atomic::AtomicBool;
use crate::config::Configs;
pub trait Context {
fn eventloop(&self);
@ -39,20 +40,20 @@ pub trait Context {
// MAC IMPLEMENTATION
#[cfg(target_os = "macos")]
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
macos::MacContext::new(send_channel, is_injecting)
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
macos::MacContext::new(config, send_channel, is_injecting)
}
// LINUX IMPLEMENTATION
#[cfg(target_os = "linux")]
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
linux::LinuxContext::new(send_channel, is_injecting)
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
linux::LinuxContext::new(config, send_channel, is_injecting)
}
// WINDOWS IMPLEMENTATION
#[cfg(target_os = "windows")]
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
windows::WindowsContext::new(send_channel, is_injecting)
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
windows::WindowsContext::new(config, send_channel, is_injecting)
}
// espanso directories

View File

@ -28,6 +28,7 @@ use log::{info, error, debug};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs;
const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp");
const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico");
@ -38,7 +39,7 @@ pub struct WindowsContext {
}
impl WindowsContext {
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<WindowsContext> {
pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<WindowsContext> {
// Initialize image resources
let espanso_dir = super::get_data_dir();

View File

@ -24,7 +24,7 @@ use crate::config::BackendType;
use crate::clipboard::ClipboardManager;
use log::{info, warn, debug, error};
use crate::ui::{UIManager, MenuItem, MenuItemType};
use crate::event::{ActionEventReceiver, ActionType};
use crate::event::{ActionEventReceiver, ActionType, SystemEventReceiver, SystemEvent};
use crate::extension::Extension;
use crate::render::{Renderer, RenderResult};
use std::cell::RefCell;
@ -334,3 +334,23 @@ impl <'a, S: KeyboardManager, C: ClipboardManager,
}
}
}
impl <'a, S: KeyboardManager, C: ClipboardManager,
M: ConfigManager<'a>, U: UIManager, R: Renderer> SystemEventReceiver for Engine<'a, S, C, M, U, R>{
fn on_system_event(&self, e: SystemEvent) {
match e {
// MacOS specific
SystemEvent::SecureInputEnabled(app_name, path) => {
info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path);
if self.config_manager.default_config().secure_input_notification {
self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
}
},
SystemEvent::SecureInputDisabled => {
info!("SecureInput has been disabled.");
},
}
}
}

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::event::{KeyEventReceiver, ActionEventReceiver, Event};
use crate::event::{KeyEventReceiver, ActionEventReceiver, Event, SystemEventReceiver};
use std::sync::mpsc::Receiver;
pub trait EventManager {
@ -28,15 +28,18 @@ pub struct DefaultEventManager<'a> {
receive_channel: Receiver<Event>,
key_receivers: Vec<&'a dyn KeyEventReceiver>,
action_receivers: Vec<&'a dyn ActionEventReceiver>,
system_receivers: Vec<&'a dyn SystemEventReceiver>,
}
impl<'a> DefaultEventManager<'a> {
pub fn new(receive_channel: Receiver<Event>, key_receivers: Vec<&'a dyn KeyEventReceiver>,
action_receivers: Vec<&'a dyn ActionEventReceiver>) -> DefaultEventManager<'a> {
action_receivers: Vec<&'a dyn ActionEventReceiver>,
system_receivers: Vec<&'a dyn SystemEventReceiver>) -> DefaultEventManager<'a> {
DefaultEventManager {
receive_channel,
key_receivers,
action_receivers,
system_receivers
}
}
}
@ -53,6 +56,9 @@ impl <'a> EventManager for DefaultEventManager<'a> {
Event::Action(action_event) => {
self.action_receivers.iter().for_each(|&receiver| receiver.on_action_event(action_event.clone()));
}
Event::System(system_event) => {
self.system_receivers.iter().for_each(move |&receiver| receiver.on_system_event(system_event.clone()));
}
}
},
Err(e) => panic!("Broken event channel {}", e),

View File

@ -24,7 +24,8 @@ use serde::{Serialize, Deserialize};
#[derive(Debug, Clone)]
pub enum Event {
Action(ActionType),
Key(KeyEvent)
Key(KeyEvent),
System(SystemEvent),
}
#[derive(Debug, Clone)]
@ -132,6 +133,13 @@ impl KeyModifier {
}
}
#[derive(Debug, Clone)]
pub enum SystemEvent {
// MacOS specific
SecureInputEnabled(String, String), // AppName, App Path
SecureInputDisabled,
}
// Receivers
pub trait KeyEventReceiver {
@ -142,6 +150,10 @@ pub trait ActionEventReceiver {
fn on_action_event(&self, e: ActionType);
}
pub trait SystemEventReceiver {
fn on_system_event(&self, e: SystemEvent);
}
// TESTS
#[cfg(test)]

View File

@ -338,7 +338,7 @@ fn daemon_main(config_set: ConfigSet) {
// we could reinterpret the characters we are injecting
let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false));
let context = context::new(send_channel.clone(), is_injecting.clone());
let context = context::new(config_set.default.clone(), send_channel.clone(), is_injecting.clone());
let config_set_copy = config_set.clone();
thread::Builder::new().name("daemon_background".to_string()).spawn(move || {
@ -382,6 +382,7 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet, is
receive_channel,
vec!(&matcher),
vec!(&engine, &matcher),
vec!(&engine),
);
info!("espanso is running!");

View File

@ -20,7 +20,7 @@
use std::os::raw::c_char;
use std::ffi::CStr;
use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier};
use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier, get_secure_input_process, get_path_from_pid};
pub struct MacSystemManager {
@ -75,70 +75,65 @@ impl MacSystemManager {
}
}
/// Check whether an application is currently holding the Secure Input.
/// Return None if no application has claimed SecureInput, its PID otherwise.
pub fn get_secure_input_pid() -> Option<i64> {
unsafe {
let mut pid: i64 = -1;
let res = get_secure_input_process(&mut pid as *mut i64);
if res > 0{
Some(pid)
}else{
None
}
}
}
/// Check whether an application is currently holding the Secure Input.
/// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise.
pub fn get_secure_input_application() -> Option<(String, String)> {
use std::process::Command;
use regex::Regex;
let output = Command::new("ioreg")
.arg("-d")
.arg("1")
.arg("-k")
.arg("IOConsoleUsers")
.arg("-w")
.arg("0")
.output();
lazy_static! {
static ref PID_REGEX: Regex = Regex::new("\"kCGSSessionSecureInputPID\"=(\\d+)").unwrap();
};
lazy_static! {
static ref APP_REGEX: Regex = Regex::new("/([^/]+).app/").unwrap();
};
if let Ok(output) = output {
let output_str = String::from_utf8_lossy(output.stdout.as_slice());
let caps = PID_REGEX.captures(&output_str);
unsafe {
let pid = MacSystemManager::get_secure_input_pid();
if let Some(caps) = caps {
// Get the PID of the process that is handling SecureInput
let pid_str = caps.get(1).map_or("", |m| m.as_str());
let pid = pid_str.parse::<i32>().expect("Invalid pid value");
if let Some(pid) = pid {
// Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant.
// the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently.
let mut buffer : [c_char; 4096] = [0; 4096];
let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32);
// Find the process that is handling the SecureInput
let output = Command::new("ps")
.arg("-p")
.arg(pid.to_string())
.arg("-o")
.arg("command=")
.output();
if res > 0 {
let c_string = CStr::from_ptr(buffer.as_ptr());
let string = c_string.to_str();
if let Ok(path) = string {
if !path.trim().is_empty() {
let process = path.trim().to_string();
let caps = APP_REGEX.captures(&process);
let app_name = if let Some(caps) = caps {
caps.get(1).map_or("", |m| m.as_str()).to_owned()
}else{
process.to_owned()
};
if let Ok(output) = output {
let output_str = String::from_utf8_lossy(output.stdout.as_slice());
if !output_str.trim().is_empty() {
let process = output_str.trim().to_string();
let caps = APP_REGEX.captures(&process);
let app_name = if let Some(caps) = caps {
caps.get(1).map_or("", |m| m.as_str()).to_owned()
Some((app_name, process))
}else{
process.to_owned()
};
Some((app_name, process))
None
}
}else{
None
}
}else{ // Can't obtain process name
}else{
None
}
}else{ // No process is holding SecureInput
}else{
None
}
}else{ // Can't execute the query to the IOKit registry
None
}
}
}

View File

@ -30,10 +30,14 @@ pub struct LinuxUIManager {
impl super::UIManager for LinuxUIManager {
fn notify(&self, message: &str) {
self.notify_delay(message, 2000);
}
fn notify_delay(&self, message: &str, duration: i32) {
let res = Command::new("notify-send")
.args(&["-i", self.icon_path.to_str().unwrap_or_default(),
"-t", "2000", "espanso", message])
.output();
.args(&["-i", self.icon_path.to_str().unwrap_or_default(),
"-t", &duration.to_string(), "espanso", message])
.output();
if let Err(e) = res {
error!("Could not send a notification, error: {}", e);

View File

@ -29,7 +29,6 @@ use std::os::raw::c_char;
use crate::context;
const NOTIFY_HELPER_BINARY : &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip");
const DEFAULT_NOTIFICATION_DELAY : f64 = 1.5;
pub struct MacUIManager {
notify_helper_path: PathBuf
@ -37,12 +36,18 @@ pub struct MacUIManager {
impl super::UIManager for MacUIManager {
fn notify(&self, message: &str) {
self.notify_delay(message, 1500);
}
fn notify_delay(&self, message: &str, duration: i32) {
let executable_path = self.notify_helper_path.join("Contents");
let executable_path = executable_path.join("MacOS");
let executable_path = executable_path.join("EspansoNotifyHelper");
let duration_float = duration as f64 / 1000.0;
let res = Command::new(executable_path)
.args(&["espanso", message, &DEFAULT_NOTIFICATION_DELAY.to_string()])
.args(&["espanso", message, &duration_float.to_string()])
.spawn();
if let Err(e) = res {

View File

@ -28,6 +28,7 @@ mod macos;
pub trait UIManager {
fn notify(&self, message: &str);
fn notify_delay(&self, message: &str, duration: i32);
fn show_menu(&self, menu: Vec<MenuItem>);
fn cleanup(&self);
}

View File

@ -31,17 +31,23 @@ pub struct WindowsUIManager {
impl super::UIManager for WindowsUIManager {
fn notify(&self, message: &str) {
self.notify_delay(message, 2000);
}
fn notify_delay(&self, message: &str, duration: i32) {
let current_id: i32 = {
let mut id = self.id.lock().unwrap();
*id += 1;
*id
};
let step = duration / 10;
// Setup a timeout to close the notification
let id = Arc::clone(&self.id);
let _ = thread::Builder::new().name("notification_thread".to_string()).spawn(move || {
for _ in 1..10 {
let duration = time::Duration::from_millis(200);
let duration = time::Duration::from_millis(step as u64);
thread::sleep(duration);
let new_id = id.lock().unwrap();