mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-22789] Handle Autotype Hotkey Event (#15840)
* [PM-22789] Handle full hotkey event for autotype * [PM-22789] Move userId into windows conditional check * [PM-22789] Refactor autotype service observables * [PM-22789] Address PR comments * [PM-22789] Refactor stringIsNotUndefinedNullAndEmpty() function
This commit is contained in:
@@ -91,6 +91,7 @@ export class InitService {
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
await this.autofillService.init();
|
||||
await this.autotypeService.init();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -455,17 +455,15 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopAutotypeService,
|
||||
useFactory: (
|
||||
configService: ConfigService,
|
||||
globalStateProvider: GlobalStateProvider,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
) =>
|
||||
new DesktopAutotypeService(
|
||||
configService,
|
||||
globalStateProvider,
|
||||
platformUtilsService.getDevice() === DeviceType.WindowsDesktop,
|
||||
),
|
||||
deps: [ConfigService, GlobalStateProvider, PlatformUtilsServiceAbstraction],
|
||||
useClass: DesktopAutotypeService,
|
||||
deps: [
|
||||
AccountService,
|
||||
AuthService,
|
||||
CipherServiceAbstraction,
|
||||
ConfigService,
|
||||
GlobalStateProvider,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,33 +1,70 @@
|
||||
import { autotype } from "@bitwarden/desktop-napi";
|
||||
import { ipcMain, globalShortcut } from "electron";
|
||||
|
||||
import { DesktopAutotypeService } from "../services/desktop-autotype.service";
|
||||
import { autotype } from "@bitwarden/desktop-napi";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
|
||||
|
||||
export class MainDesktopAutotypeService {
|
||||
constructor(private desktopAutotypeService: DesktopAutotypeService) {}
|
||||
keySequence: string = "Alt+CommandOrControl+I";
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.desktopAutotypeService.autotypeEnabled$.subscribe((enabled) => {
|
||||
if (enabled) {
|
||||
ipcMain.on("autofill.configureAutotype", (event, data) => {
|
||||
if (data.enabled === true && !globalShortcut.isRegistered(this.keySequence)) {
|
||||
this.enableAutotype();
|
||||
} else {
|
||||
} else if (data.enabled === false && globalShortcut.isRegistered(this.keySequence)) {
|
||||
this.disableAutotype();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completeAutotypeRequest", (event, data) => {
|
||||
const { response } = data;
|
||||
|
||||
if (
|
||||
stringIsNotUndefinedNullAndEmpty(response.username) &&
|
||||
stringIsNotUndefinedNullAndEmpty(response.password)
|
||||
) {
|
||||
this.doAutotype(response.username, response.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: this will call into desktop native code
|
||||
private enableAutotype() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Enabling Autotype...");
|
||||
|
||||
const result = autotype.getForegroundWindowTitle();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Window Title: " + result);
|
||||
disableAutotype() {
|
||||
if (globalShortcut.isRegistered(this.keySequence)) {
|
||||
globalShortcut.unregister(this.keySequence);
|
||||
}
|
||||
|
||||
// TODO: this will call into desktop native code
|
||||
private disableAutotype() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Disabling Autotype...");
|
||||
this.logService.info("Autotype disabled.");
|
||||
}
|
||||
|
||||
private enableAutotype() {
|
||||
const result = globalShortcut.register(this.keySequence, () => {
|
||||
const windowTitle = autotype.getForegroundWindowTitle();
|
||||
|
||||
this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", {
|
||||
windowTitle,
|
||||
});
|
||||
});
|
||||
|
||||
result
|
||||
? this.logService.info("Autotype enabled.")
|
||||
: this.logService.info("Enabling autotype failed.");
|
||||
}
|
||||
|
||||
private doAutotype(username: string, password: string) {
|
||||
const inputPattern = username + "\t" + password;
|
||||
const inputArray = new Array<number>(inputPattern.length);
|
||||
|
||||
for (let i = 0; i < inputPattern.length; i++) {
|
||||
inputArray[i] = inputPattern.charCodeAt(i);
|
||||
}
|
||||
|
||||
autotype.typeInput(inputArray);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,4 +127,43 @@ export default {
|
||||
},
|
||||
);
|
||||
},
|
||||
configureAutotype: (enabled: boolean) => {
|
||||
ipcRenderer.send("autofill.configureAutotype", { enabled });
|
||||
},
|
||||
listenAutotypeRequest: (
|
||||
fn: (
|
||||
windowTitle: string,
|
||||
completeCallback: (
|
||||
error: Error | null,
|
||||
response: { username?: string; password?: string },
|
||||
) => void,
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.listenAutotypeRequest",
|
||||
(
|
||||
event,
|
||||
data: {
|
||||
windowTitle: string;
|
||||
},
|
||||
) => {
|
||||
const { windowTitle } = data;
|
||||
|
||||
fn(windowTitle, (error, response) => {
|
||||
if (error) {
|
||||
ipcRenderer.send("autofill.completeError", {
|
||||
windowTitle,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send("autofill.completeAutotypeRequest", {
|
||||
windowTitle,
|
||||
response,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { combineLatest, map, Observable, of } from "rxjs";
|
||||
import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
GlobalStateProvider,
|
||||
AUTOTYPE_SETTINGS_DISK,
|
||||
KeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export const AUTOTYPE_ENABLED = new KeyDefinition<boolean>(
|
||||
AUTOTYPE_SETTINGS_DISK,
|
||||
@@ -20,28 +28,83 @@ export class DesktopAutotypeService {
|
||||
autotypeEnabled$: Observable<boolean> = of(false);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private configService: ConfigService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private isWindows: boolean,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
if (this.isWindows) {
|
||||
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
|
||||
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
|
||||
const firstCipher = possibleCiphers?.at(0);
|
||||
|
||||
return callback(null, {
|
||||
username: firstCipher?.login?.username,
|
||||
password: firstCipher?.login?.password,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) {
|
||||
this.autotypeEnabled$ = combineLatest([
|
||||
this.autotypeEnabledState.state$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
switchMap((userId) => this.authService.authStatusFor$(userId)),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag]) =>
|
||||
autotypeEnabled && windowsDesktopAutotypeFeatureFlag,
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus]) =>
|
||||
autotypeEnabled &&
|
||||
windowsDesktopAutotypeFeatureFlag &&
|
||||
authStatus == AuthenticationStatus.Unlocked,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
init() {}
|
||||
this.autotypeEnabled$.subscribe((enabled) => {
|
||||
ipc.autofill.configureAutotype(enabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setAutotypeEnabledState(enabled: boolean): Promise<void> {
|
||||
await this.autotypeEnabledState.update(() => enabled, {
|
||||
shouldUpdate: (currentlyEnabled) => currentlyEnabled !== enabled,
|
||||
});
|
||||
}
|
||||
|
||||
async matchCiphersToWindowTitle(windowTitle: string): Promise<CipherView[]> {
|
||||
const URI_PREFIX = "APP:";
|
||||
windowTitle = windowTitle.toLowerCase();
|
||||
|
||||
const ciphers = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) => this.cipherService.cipherViews$(userId)),
|
||||
),
|
||||
);
|
||||
|
||||
const possibleCiphers = ciphers.filter((c) => {
|
||||
return (
|
||||
c.login?.username &&
|
||||
c.login?.password &&
|
||||
c.deletedDate == null &&
|
||||
c.login?.uris.some((u) => {
|
||||
if (u.uri?.indexOf(URI_PREFIX) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uri = u.uri.substring(4).toLowerCase();
|
||||
|
||||
return windowTitle.indexOf(uri) > -1;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return possibleCiphers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-
|
||||
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
|
||||
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
|
||||
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutotypeService } from "./autofill/services/desktop-autotype.service";
|
||||
import { DesktopBiometricsService } from "./key-management/biometrics/desktop.biometrics.service";
|
||||
import { MainBiometricsIPCListener } from "./key-management/biometrics/main-biometrics-ipc.listener";
|
||||
import { MainBiometricsService } from "./key-management/biometrics/main-biometrics.service";
|
||||
@@ -48,7 +47,6 @@ import { PowerMonitorMain } from "./main/power-monitor.main";
|
||||
import { TrayMain } from "./main/tray.main";
|
||||
import { UpdaterMain } from "./main/updater.main";
|
||||
import { WindowMain } from "./main/window.main";
|
||||
import { SlimConfigService } from "./platform/config/slim-config.service";
|
||||
import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main";
|
||||
import { ClipboardMain } from "./platform/main/clipboard.main";
|
||||
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
|
||||
@@ -307,13 +305,22 @@ export class Main {
|
||||
void this.nativeAutofillMain.init();
|
||||
|
||||
this.mainDesktopAutotypeService = new MainDesktopAutotypeService(
|
||||
new DesktopAutotypeService(
|
||||
new SlimConfigService(this.environmentService, globalStateProvider),
|
||||
globalStateProvider,
|
||||
process.platform === "win32",
|
||||
),
|
||||
this.logService,
|
||||
this.windowMain,
|
||||
);
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
this.mainDesktopAutotypeService.init();
|
||||
})
|
||||
.catch((reason) => {
|
||||
this.logService.error("Error initializing Autotype.", reason);
|
||||
});
|
||||
|
||||
app.on("will-quit", () => {
|
||||
this.mainDesktopAutotypeService.disableAutotype();
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { combineLatest, map, Observable, throwError } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import {
|
||||
FeatureFlag,
|
||||
FeatureFlagValueType,
|
||||
getFeatureFlagValue,
|
||||
} from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { ServerSettings } from "@bitwarden/common/platform/models/domain/server-settings";
|
||||
import { GLOBAL_SERVER_CONFIGURATIONS } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
/*
|
||||
NOT FOR GENERAL USE
|
||||
|
||||
If you have more uses for the config service in the main process,
|
||||
please reach out to platform.
|
||||
*/
|
||||
export class SlimConfigService implements ConfigService {
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
) {}
|
||||
|
||||
serverConfig$: Observable<ServerConfig> = throwError(() => {
|
||||
return new Error("Method not implemented.");
|
||||
});
|
||||
serverSettings$: Observable<ServerSettings> = throwError(() => {
|
||||
return new Error("Method not implemented.");
|
||||
});
|
||||
cloudRegion$: Observable<Region> = throwError(() => {
|
||||
return new Error("Method not implemented.");
|
||||
});
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag): Observable<FeatureFlagValueType<Flag>> {
|
||||
return combineLatest([
|
||||
this.environmentService.environment$,
|
||||
this.globalStateProvider.get(GLOBAL_SERVER_CONFIGURATIONS).state$,
|
||||
]).pipe(
|
||||
map(([environment, serverConfigMap]) =>
|
||||
getFeatureFlagValue(serverConfigMap?.[environment.getApiUrl()], key),
|
||||
),
|
||||
);
|
||||
}
|
||||
userCachedFeatureFlag$<Flag extends FeatureFlag>(
|
||||
key: Flag,
|
||||
userId: UserId,
|
||||
): Observable<FeatureFlagValueType<Flag>> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getFeatureFlag<Flag extends FeatureFlag>(key: Flag): Promise<FeatureFlagValueType<Flag>> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer): Observable<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
ensureConfigFetched(): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -98,3 +98,11 @@ export function cleanUserAgent(userAgent: string): string {
|
||||
.replace(userAgentItem("Bitwarden", " "), "")
|
||||
.replace(userAgentItem("Electron", " "), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the provided string is not undefined, not null, and not empty.
|
||||
* Otherwise, returns `false`.
|
||||
*/
|
||||
export function stringIsNotUndefinedNullAndEmpty(str: string): boolean {
|
||||
return str?.length > 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user