diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 05663ea7e0b..944ef26231d 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -162,7 +162,7 @@ dependencies = [ "serde_repr", "tokio", "url", - "zbus 5.6.0", + "zbus", ] [[package]] @@ -362,6 +362,15 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "autotype" +version = "0.0.0" +dependencies = [ + "anyhow", + "windows 0.61.1", + "windows-core 0.61.0", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -900,7 +909,7 @@ dependencies = [ "widestring", "windows 0.61.1", "windows-future", - "zbus 4.4.0", + "zbus", "zbus_polkit", "zeroizing-alloc", ] @@ -910,6 +919,7 @@ name = "desktop_napi" version = "0.0.0" dependencies = [ "anyhow", + "autotype", "base64", "desktop_core", "hex", @@ -2063,10 +2073,10 @@ dependencies = [ "sha2", "subtle", "tokio", - "zbus 5.6.0", - "zbus_macros 5.6.0", + "zbus", + "zbus_macros", "zeroize", - "zvariant 5.5.1", + "zvariant", ] [[package]] @@ -2715,17 +2725,6 @@ dependencies = [ "syn", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.8" @@ -3921,9 +3920,9 @@ dependencies = [ [[package]] name = "zbus" -version = "4.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", "async-executor", @@ -3938,90 +3937,37 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros 4.4.0", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus" -version = "5.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2522b82023923eecb0b366da727ec883ace092e7887b61d3da5139f26b44da58" -dependencies = [ - "async-broadcast", - "async-recursion", - "async-trait", - "enumflags2", - "event-listener", - "futures-core", "futures-lite", "hex", "nix", "ordered-stream", "serde", "serde_repr", + "static_assertions", "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", "winnow", - "zbus_macros 5.6.0", - "zbus_names 4.2.0", - "zvariant 5.5.1", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", - "zvariant_utils 2.1.0", -] - -[[package]] -name = "zbus_macros" -version = "5.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d2e12843c75108c00c618c2e8ef9675b50b6ec095b36dc965f2e5aed463c15" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zbus_names 4.2.0", - "zvariant 5.5.1", - "zvariant_utils 3.2.0", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant 4.2.0", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] @@ -4033,20 +3979,20 @@ dependencies = [ "serde", "static_assertions", "winnow", - "zvariant 5.5.1", + "zvariant", ] [[package]] name = "zbus_polkit" -version = "4.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a29bfa927b29f91b7feb4e1990f2dd1b4604072f493dc2f074cf59e4e0ba90" +checksum = "ad23d5c4d198c7e2641b33e6e0d1f866f117408ba66fe80bbe52e289eeb77c52" dependencies = [ "enumflags2", "serde", "serde_repr", "static_assertions", - "zbus 4.4.0", + "zbus", ] [[package]] @@ -4149,19 +4095,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zvariant" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive 4.2.0", -] - [[package]] name = "zvariant" version = "5.5.1" @@ -4173,21 +4106,8 @@ dependencies = [ "serde", "url", "winnow", - "zvariant_derive 5.5.1", - "zvariant_utils 3.2.0", -] - -[[package]] -name = "zvariant_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", - "zvariant_utils 2.1.0", + "zvariant_derive", + "zvariant_utils", ] [[package]] @@ -4200,18 +4120,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils 3.2.0", -] - -[[package]] -name = "zvariant_utils" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zvariant_utils", ] [[package]] diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 451704f91fe..ff07101546b 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["napi", "core", "proxy", "macos_provider", "windows_plugin_authenticator"] +members = ["napi", "core", "proxy", "macos_provider", "windows_plugin_authenticator", "autotype"] [workspace.package] version = "0.0.0" diff --git a/apps/desktop/desktop_native/autotype/Cargo.toml b/apps/desktop/desktop_native/autotype/Cargo.toml new file mode 100644 index 00000000000..ad005684265 --- /dev/null +++ b/apps/desktop/desktop_native/autotype/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "autotype" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +publish = { workspace = true } + +[dependencies] +anyhow = { workspace = true } + +[target.'cfg(windows)'.dependencies] +windows = { workspace = true, features = ["Win32_Foundation", "Win32_UI_WindowsAndMessaging" ] } +windows-core = { workspace = true } diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs new file mode 100644 index 00000000000..0afa77a1141 --- /dev/null +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -0,0 +1,164 @@ +#![cfg(target_os = "windows")] + +use anyhow::Result; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use windows::Win32::Foundation::{HWND, LPARAM}; +use windows::Win32::UI::Input::KeyboardAndMouse::{ + SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, VIRTUAL_KEY, VK_RETURN, VK_TAB, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + EnumWindows, GetForegroundWindow, GetWindowTextW, IsWindowVisible, SetForegroundWindow, ShowWindow, SW_NORMAL, +}; +use windows_core::BOOL; + +#[derive(Debug, Clone)] +pub struct WindowInfo { + pub handle: isize, + pub title: String, +} + +pub fn get_active_windows() -> Result> { + extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { + unsafe { + if IsWindowVisible(hwnd).as_bool() { + let mut buf = [0u16; 512]; + let len = GetWindowTextW(hwnd, &mut buf); + if len > 0 { + let title = OsString::from_wide(&buf[..len as usize]) + .to_string_lossy() + .into_owned(); + let windows = &mut *(lparam.0 as *mut Vec); + windows.push(WindowInfo { + handle: hwnd.0 as isize, + title, + }); + } + } + } + true.into() + } + + let mut windows: Vec = Vec::new(); + unsafe { + let _ = EnumWindows( + Some(enum_windows_proc), + LPARAM(&mut windows as *mut _ as isize), + ); + } + Ok(windows) +} + +pub fn set_window_foreground(handle: isize) -> Result<()> { + unsafe { + let hwnd = HWND(handle as *mut std::ffi::c_void); + let _ = ShowWindow(hwnd, SW_NORMAL); + if !SetForegroundWindow(hwnd).as_bool() { + anyhow::bail!("Failed to set window foreground"); + } + } + Ok(()) +} + +const DELAY_MS: u64 = 25; // Delay between keystrokes +const DELAY_MS_STEPS: u64 = 200; // Delay between steps + +fn send_virtual_key(vk: VIRTUAL_KEY, press: bool) -> Result<()> { + let mut input = INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 { + ki: KEYBDINPUT { + wVk: vk, + wScan: 0, + dwFlags: if !press { + KEYEVENTF_KEYUP + } else { + Default::default() + }, + time: 0, + dwExtraInfo: 0, + }, + }, + }; + + unsafe { + SendInput(&[input], std::mem::size_of::() as i32); + } + Ok(()) +} + +fn send_unicode_char(c: char) -> Result<()> { + let mut input = INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 { + ki: KEYBDINPUT { + wVk: VIRTUAL_KEY(0), + wScan: c as u16, + dwFlags: windows::Win32::UI::Input::KeyboardAndMouse::KEYEVENTF_UNICODE, + time: 0, + dwExtraInfo: 0, + }, + }, + }; + + unsafe { + SendInput(&[input], std::mem::size_of::() as i32); + } + std::thread::sleep(std::time::Duration::from_millis(DELAY_MS)); + Ok(()) +} + +pub fn perform_autotype(username: &str, password: &str, send_enter: bool) -> Result<()> { + // Type username + for c in username.chars() { + send_unicode_char(c)?; + } + + std::thread::sleep(std::time::Duration::from_millis(DELAY_MS_STEPS)); + + // Press TAB + send_virtual_key(VK_TAB, true)?; + send_virtual_key(VK_TAB, false)?; + std::thread::sleep(std::time::Duration::from_millis(DELAY_MS)); + + std::thread::sleep(std::time::Duration::from_millis(DELAY_MS_STEPS)); + + // Type password + for c in password.chars() { + send_unicode_char(c)?; + } + + std::thread::sleep(std::time::Duration::from_millis(DELAY_MS_STEPS)); + + // Optionally press Enter + if send_enter { + send_virtual_key(VK_RETURN, true)?; + send_virtual_key(VK_RETURN, false)?; + } + + Ok(()) +} + +pub fn get_focused_window() -> Result { + unsafe { + let hwnd = GetForegroundWindow(); + if hwnd.0.is_null() { + anyhow::bail!("No foreground window found"); + } + + let mut buf = [0u16; 512]; + let len = GetWindowTextW(hwnd, &mut buf); + if len == 0 { + anyhow::bail!("Failed to get window title"); + } + + let title = OsString::from_wide(&buf[..len as usize]) + .to_string_lossy() + .into_owned(); + + Ok(WindowInfo { + handle: hwnd.0 as isize, + title, + }) + } +} diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 669f166e748..0195448fd3e 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -30,6 +30,7 @@ tokio-stream = { workspace = true } [target.'cfg(windows)'.dependencies] windows-registry = { workspace = true } windows_plugin_authenticator = { path = "../windows_plugin_authenticator" } +autotype = { path = "../autotype" } [build-dependencies] napi-build = { workspace = true } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index b3c6f715e98..649ad9322ab 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -198,3 +198,13 @@ export declare namespace logging { } export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void } +export declare namespace autotype { + export interface WindowInfo { + handle: number + title: string + } + export function getActiveWindows(): Promise> + export function getFocusedWindow(): Promise + export function setWindowForeground(handle: number): Promise + export function performAutotype(username: string, password: string, sendEnter: boolean): Promise +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 079872a3b03..a1164a4a853 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -875,3 +875,57 @@ pub mod logging { fn flush(&self) {} } } + +#[napi] +pub mod autotype { + use autotype::WindowInfo as _WindowInfo; + + #[napi(object)] + pub struct WindowInfo { + pub handle: u32, + pub title: String, + } + + impl From<_WindowInfo> for WindowInfo { + fn from(w: _WindowInfo) -> Self { + WindowInfo { + handle: w.handle as u32, + title: w.title, + } + } + } + + #[napi] + pub async fn get_active_windows() -> napi::Result> { + autotype::get_active_windows() + .map(|windows| windows.into_iter().map(WindowInfo::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn get_focused_window() -> napi::Result { + autotype::get_focused_window() + .map(WindowInfo::from) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn set_window_foreground(handle: u32) -> napi::Result<()> { + autotype::set_window_foreground(handle as isize).map_err(|e| { + napi::Error::from_reason(format!( + "Setting window foreground failed - Error: {e} - {e:?}" + )) + }) + } + + #[napi] + pub async fn perform_autotype( + username: String, + password: String, + send_enter: bool, + ) -> napi::Result<()> { + autotype::perform_autotype(&username, &password, send_enter).map_err(|e| { + napi::Error::from_reason(format!("Perform autotype failed - Error: {e} - {e:?}")) + }) + } +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5e0ea7f9fac..5a1f3a4d0cb 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -39,6 +39,7 @@ import { MenuMain } from "./main/menu/menu.main"; import { MessagingMain } from "./main/messaging.main"; import { NativeMessagingMain } from "./main/native-messaging.main"; import { PowerMonitorMain } from "./main/power-monitor.main"; +import { ShortcutsMain } from "./main/shortcuts.main"; import { TrayMain } from "./main/tray.main"; import { UpdaterMain } from "./main/updater.main"; import { WindowMain } from "./main/window.main"; @@ -53,6 +54,7 @@ import { ElectronStorageService } from "./platform/services/electron-storage.ser import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service"; import { I18nMainService } from "./platform/services/i18n.main.service"; import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service"; +import { AutotypeService } from "./services/autotype.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; import { isMacAppStore } from "./utils"; @@ -75,6 +77,7 @@ export class Main { messagingMain: MessagingMain; updaterMain: UpdaterMain; menuMain: MenuMain; + shortcutsMain: ShortcutsMain; powerMonitorMain: PowerMonitorMain; trayMain: TrayMain; biometricsService: DesktopBiometricsService; @@ -105,6 +108,10 @@ export class Main { // on ready stuff... }); + app.on("will-quit", () => { + this.shortcutsMain.destroy(); + }); + if (appDataPath != null) { app.setPath("userData", appDataPath); } @@ -286,6 +293,8 @@ export class Main { this.ssoUrlService, ); + this.shortcutsMain = new ShortcutsMain(this, new AutotypeService()); + this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain); void this.nativeAutofillMain.init(); } @@ -318,6 +327,7 @@ export class Main { } this.powerMonitorMain.init(); await this.updaterMain.init(); + await this.shortcutsMain.init(); const [browserIntegrationEnabled, ddgIntegrationEnabled] = await Promise.all([ firstValueFrom(this.desktopSettingsService.browserIntegrationEnabled$), diff --git a/apps/desktop/src/main/shortcuts.main.ts b/apps/desktop/src/main/shortcuts.main.ts new file mode 100644 index 00000000000..cacf5b6072c --- /dev/null +++ b/apps/desktop/src/main/shortcuts.main.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ + +import { globalShortcut } from "electron"; + +import { Main } from "../main"; +import { AutotypeService } from "../services/autotype.service"; + +export class ShortcutsMain { + constructor( + private main: Main, + private autotypeService: AutotypeService, + ) {} + + async init() { + globalShortcut.register("CommandOrControl+Shift+L", async () => { + console.log("Autofill shortcut triggered!"); + const focusedWindow = await this.autotypeService.getFocusedWindow(); + console.log(focusedWindow); + // TODO: Look up cipher with focusedWindow.title + const shouldAutofill = focusedWindow.title.indexOf("Notepad") > -1; + if (shouldAutofill) { + await this.autotypeService.performAutotype("testuser", "testpassword", true); + } + }); + + globalShortcut.register("CommandOrControl+Shift+J", async () => { + console.log("Autofill to window shortcut triggered!"); + + const currentWindows = await this.autotypeService.getActiveWindows(); + let windowId: number = null; + for (const w of currentWindows) { + console.log(`Window ID: ${w.handle}, Title: ${w.title}`); + if (w.title.indexOf("Notepad") > -1) { + windowId = w.handle; + break; + } + } + + if (windowId != null) { + await this.autotypeService.setWindowForeground(windowId); + } + + const focusedWindow = await this.autotypeService.getFocusedWindow(); + if (focusedWindow.handle === windowId) { + await this.autotypeService.performAutotype("testuser", "testpassword", true); + } + }); + } + + destroy() { + globalShortcut.unregisterAll(); + } +} diff --git a/apps/desktop/src/services/autotype.service.ts b/apps/desktop/src/services/autotype.service.ts new file mode 100644 index 00000000000..56dd61a1b7a --- /dev/null +++ b/apps/desktop/src/services/autotype.service.ts @@ -0,0 +1,39 @@ +import { ipcMain } from "electron"; + +import { autotype } from "@bitwarden/desktop-napi"; + +export class AutotypeService { + constructor() { + ipcMain.handle("autotype.getActiveWindows", async (event) => { + return await this.getActiveWindows(); + }); + ipcMain.handle("autotype.getFocusedWindow", async (event) => { + return await this.getFocusedWindow(); + }); + ipcMain.handle("autotype.setWindowForeground", async (event, handle: number) => { + return await this.setWindowForeground(handle); + }); + ipcMain.handle( + "autotype.performAutotype", + async (event, username: string, password: string, sendEnter: boolean) => { + return await this.performAutotype(username, password, sendEnter); + }, + ); + } + + async getActiveWindows() { + return await autotype.getActiveWindows(); + } + + async getFocusedWindow() { + return await autotype.getFocusedWindow(); + } + + async setWindowForeground(handle: number) { + return await autotype.setWindowForeground(handle); + } + + async performAutotype(username: string, password: string, sendEnter: boolean) { + return await autotype.performAutotype(username, password, sendEnter); + } +}