diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 8a193218e4b..698427c1e57 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -91,6 +91,7 @@ export class InitService { containerService.attachToGlobal(this.win); await this.autofillService.init(); + await this.autotypeService.init(); }; } } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 4482c38fc3a..95d1f4643fa 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -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, + ], }), ]; diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index e0871f86f8c..592d1e1b7a8 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -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..."); + disableAutotype() { + if (globalShortcut.isRegistered(this.keySequence)) { + globalShortcut.unregister(this.keySequence); + } - const result = autotype.getForegroundWindowTitle(); - // eslint-disable-next-line no-console - console.log("Window Title: " + result); + this.logService.info("Autotype disabled."); } - // TODO: this will call into desktop native code - private disableAutotype() { - // eslint-disable-next-line no-console - console.log("Disabling Autotype..."); + 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(inputPattern.length); + + for (let i = 0; i < inputPattern.length; i++) { + inputArray[i] = inputPattern.charCodeAt(i); + } + + autotype.typeInput(inputArray); } } diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 2c006b5c928..af238b17e80 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -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, + }); + }); + }, + ); + }, }; diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 4a00cec465a..65152450a86 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -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( AUTOTYPE_SETTINGS_DISK, @@ -20,28 +28,83 @@ export class DesktopAutotypeService { autotypeEnabled$: Observable = 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, ), ); + + this.autotypeEnabled$.subscribe((enabled) => { + ipc.autofill.configureAutotype(enabled); + }); } } - init() {} - async setAutotypeEnabledState(enabled: boolean): Promise { await this.autotypeEnabledState.update(() => enabled, { shouldUpdate: (currentlyEnabled) => currentlyEnabled !== enabled, }); } + + async matchCiphersToWindowTitle(windowTitle: string): Promise { + 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; + } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 67fbf457a77..deb09d2f335 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -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, ); - this.mainDesktopAutotypeService.init(); + + app + .whenReady() + .then(() => { + this.mainDesktopAutotypeService.init(); + }) + .catch((reason) => { + this.logService.error("Error initializing Autotype.", reason); + }); + + app.on("will-quit", () => { + this.mainDesktopAutotypeService.disableAutotype(); + }); } bootstrap() { diff --git a/apps/desktop/src/platform/config/slim-config.service.ts b/apps/desktop/src/platform/config/slim-config.service.ts deleted file mode 100644 index 9a535ba9dfa..00000000000 --- a/apps/desktop/src/platform/config/slim-config.service.ts +++ /dev/null @@ -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 = throwError(() => { - return new Error("Method not implemented."); - }); - serverSettings$: Observable = throwError(() => { - return new Error("Method not implemented."); - }); - cloudRegion$: Observable = throwError(() => { - return new Error("Method not implemented."); - }); - getFeatureFlag$(key: Flag): Observable> { - return combineLatest([ - this.environmentService.environment$, - this.globalStateProvider.get(GLOBAL_SERVER_CONFIGURATIONS).state$, - ]).pipe( - map(([environment, serverConfigMap]) => - getFeatureFlagValue(serverConfigMap?.[environment.getApiUrl()], key), - ), - ); - } - userCachedFeatureFlag$( - key: Flag, - userId: UserId, - ): Observable> { - throw new Error("Method not implemented."); - } - getFeatureFlag(key: Flag): Promise> { - throw new Error("Method not implemented."); - } - checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer): Observable { - throw new Error("Method not implemented."); - } - ensureConfigFetched(): Promise { - throw new Error("Method not implemented."); - } -} diff --git a/apps/desktop/src/utils.ts b/apps/desktop/src/utils.ts index c798faac36e..de5cd2daebc 100644 --- a/apps/desktop/src/utils.ts +++ b/apps/desktop/src/utils.ts @@ -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; +}