1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 09:33:22 +00:00

POC for autotype

This commit is contained in:
Kyle Spearrin
2025-06-18 10:05:58 -04:00
parent b31fcc9442
commit 56f432f8c9
10 changed files with 379 additions and 126 deletions

View File

@@ -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]]

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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<Vec<WindowInfo>> {
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<WindowInfo>);
windows.push(WindowInfo {
handle: hwnd.0 as isize,
title,
});
}
}
}
true.into()
}
let mut windows: Vec<WindowInfo> = 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::<INPUT>() 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::<INPUT>() 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<WindowInfo> {
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,
})
}
}

View File

@@ -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 }

View File

@@ -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<Array<WindowInfo>>
export function getFocusedWindow(): Promise<WindowInfo>
export function setWindowForeground(handle: number): Promise<void>
export function performAutotype(username: string, password: string, sendEnter: boolean): Promise<void>
}

View File

@@ -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<Vec<WindowInfo>> {
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<WindowInfo> {
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:?}"))
})
}
}

View File

@@ -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$),

View File

@@ -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();
}
}

View File

@@ -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);
}
}