From 8379f438fe56c79ad2f3fa65b13d8bcbf122d4db Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:45:51 -0400 Subject: [PATCH 01/15] Add generate command --- apps/browser/src/background.ts | 2 + .../browser/src/background/main.background.ts | 14 +- .../autofill-service.factory.ts | 37 +++ .../cipher-service.factory.ts | 32 +-- .../crypto-service.factory.ts | 5 +- .../event-service.factory.ts | 45 ++++ .../password-generation-service.factory.ts | 31 +++ .../service_factories/totp-service.factory.ts | 31 +++ apps/browser/src/browser/sendTabsMessage.ts | 9 + apps/browser/src/content/miscUtils.ts | 24 ++ .../browser-session.decorator.ts | 11 +- apps/browser/src/globals.d.ts | 128 ++++++++- apps/browser/src/listeners/onAlarmListener.ts | 15 ++ .../src/listeners/onCommandListener.ts | 254 ++++++++++-------- apps/browser/src/manifest.v3.json | 9 +- .../services/browserPlatformUtils.service.ts | 8 +- apps/browser/src/types/tab-messages.ts | 9 + apps/browser/webpack.config.js | 2 + .../passwordGeneration.service.ts | 22 +- .../models/domain/passwordGeneratorOptions.ts | 17 ++ .../services/passwordGeneration.service.ts | 24 +- 21 files changed, 568 insertions(+), 161 deletions(-) create mode 100644 apps/browser/src/background/service_factories/autofill-service.factory.ts create mode 100644 apps/browser/src/background/service_factories/event-service.factory.ts create mode 100644 apps/browser/src/background/service_factories/password-generation-service.factory.ts create mode 100644 apps/browser/src/background/service_factories/totp-service.factory.ts create mode 100644 apps/browser/src/browser/sendTabsMessage.ts create mode 100644 apps/browser/src/content/miscUtils.ts create mode 100644 apps/browser/src/listeners/onAlarmListener.ts create mode 100644 apps/browser/src/types/tab-messages.ts create mode 100644 libs/common/src/models/domain/passwordGeneratorOptions.ts diff --git a/apps/browser/src/background.ts b/apps/browser/src/background.ts index ebc2aad10ad5..b73c3647e8d5 100644 --- a/apps/browser/src/background.ts +++ b/apps/browser/src/background.ts @@ -1,4 +1,5 @@ import MainBackground from "./background/main.background"; +import { onAlarmListener } from "./listeners/onAlarmListener"; import { onCommandListener } from "./listeners/onCommandListener"; import { onInstallListener } from "./listeners/onInstallListener"; @@ -7,6 +8,7 @@ const manifest = chrome.runtime.getManifest(); if (manifest.manifest_version === 3) { chrome.commands.onCommand.addListener(onCommandListener); chrome.runtime.onInstalled.addListener(onInstallListener); + chrome.alarms.onAlarm.addListener(onAlarmListener); } else { const bitwardenMain = ((window as any).bitwardenMain = new MainBackground()); bitwardenMain.bootstrap().then(() => { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 99114bc007f8..8c3bf97aa7a9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -109,6 +109,8 @@ import RuntimeBackground from "./runtime.background"; import TabsBackground from "./tabs.background"; import WebRequestBackground from "./webRequest.background"; +type CommonSidebarAction = typeof chrome.sidebarAction | OperaSidebarAction; + export default class MainBackground { messagingService: MessagingServiceAbstraction; storageService: AbstractStorageService; @@ -174,7 +176,7 @@ export default class MainBackground { private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; - private sidebarAction: any; + private sidebarAction: CommonSidebarAction; private buildingContextMenu: boolean; private menuOptionsLoaded: any[] = []; private syncTimeout: any; @@ -464,7 +466,7 @@ export default class MainBackground { ? null : typeof opr !== "undefined" && opr.sidebarAction ? opr.sidebarAction - : (window as any).chrome.sidebarAction; + : window.chrome.sidebarAction; // Background this.runtimeBackground = new RuntimeBackground( @@ -996,7 +998,11 @@ export default class MainBackground { }); } - private async actionSetIcon(theAction: any, suffix: string, windowId?: number): Promise { + private async actionSetIcon( + theAction: CommonSidebarAction | typeof chrome.browserAction, + suffix: string, + windowId?: number + ): Promise { if (!theAction || !theAction.setIcon) { return; } @@ -1042,7 +1048,7 @@ export default class MainBackground { return; } - if (this.sidebarAction.setBadgeText) { + if ("setBadgeText" in this.sidebarAction) { this.sidebarAction.setBadgeText({ text: text, tabId: tabId, diff --git a/apps/browser/src/background/service_factories/autofill-service.factory.ts b/apps/browser/src/background/service_factories/autofill-service.factory.ts new file mode 100644 index 000000000000..9de66856ddd9 --- /dev/null +++ b/apps/browser/src/background/service_factories/autofill-service.factory.ts @@ -0,0 +1,37 @@ +import { AutofillService as AbstractAutofillService } from "../../services/abstractions/autofill.service"; +import AutofillService from "../../services/autofill.service"; + +import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory"; +import { eventServiceFactory, EventServiceInitOptions } from "./event-service.factory"; +import { CachedServices, factory, FactoryOptions } from "./factory-options"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; +import { totpServiceFactory, TotpServiceInitOptions } from "./totp-service.factory"; + +type AutofillServiceFactoryOptions = FactoryOptions; + +export type AutofillServiceInitOptions = AutofillServiceFactoryOptions & + CipherServiceInitOptions & + StateServiceInitOptions & + TotpServiceInitOptions & + EventServiceInitOptions & + LogServiceInitOptions; + +export function autofillServiceFactory( + cache: { autofillService?: AbstractAutofillService } & CachedServices, + opts: AutofillServiceInitOptions +): Promise { + return factory( + cache, + "autofillService", + opts, + async () => + new AutofillService( + await cipherServiceFactory(cache, opts), + await stateServiceFactory(cache, opts), + await totpServiceFactory(cache, opts), + await eventServiceFactory(cache, opts), + await logServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/background/service_factories/cipher-service.factory.ts b/apps/browser/src/background/service_factories/cipher-service.factory.ts index 149ac54fc820..d449327010b6 100644 --- a/apps/browser/src/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/background/service_factories/cipher-service.factory.ts @@ -33,22 +33,18 @@ export function cipherServiceFactory( cache: { cipherService?: AbstractCipherService } & CachedServices, opts: CipherServiceInitOptions ): Promise { - return factory( - cache, - "cipherService", - opts, - async () => - new CipherService( - await cryptoServiceFactory(cache, opts), - await settingsServiceFactory(cache, opts), - await apiServiceFactory(cache, opts), - await fileUploadServiceFactory(cache, opts), - await i18nServiceFactory(cache, opts), - opts.cipherServiceOptions.searchServiceFactory === undefined - ? () => cache.searchService - : opts.cipherServiceOptions.searchServiceFactory, - await logServiceFactory(cache, opts), - await stateServiceFactory(cache, opts) - ) - ); + return factory(cache, "cipherService", opts, async () => { + return new CipherService( + await cryptoServiceFactory(cache, opts), + await settingsServiceFactory(cache, opts), + await apiServiceFactory(cache, opts), + await fileUploadServiceFactory(cache, opts), + await i18nServiceFactory(cache, opts), + opts.cipherServiceOptions?.searchServiceFactory === undefined + ? () => cache.searchService + : opts.cipherServiceOptions.searchServiceFactory, + await logServiceFactory(cache, opts), + await stateServiceFactory(cache, opts) + ); + }); } diff --git a/apps/browser/src/background/service_factories/crypto-service.factory.ts b/apps/browser/src/background/service_factories/crypto-service.factory.ts index b61b72ec04bd..784314b12d24 100644 --- a/apps/browser/src/background/service_factories/crypto-service.factory.ts +++ b/apps/browser/src/background/service_factories/crypto-service.factory.ts @@ -1,5 +1,6 @@ import { CryptoService as AbstractCryptoService } from "@bitwarden/common/abstractions/crypto.service"; -import { CryptoService } from "@bitwarden/common/services/crypto.service"; + +import { BrowserCryptoService } from "../../services/browserCrypto.service"; import { cryptoFunctionServiceFactory, @@ -32,7 +33,7 @@ export function cryptoServiceFactory( "cryptoService", opts, async () => - new CryptoService( + new BrowserCryptoService( await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), diff --git a/apps/browser/src/background/service_factories/event-service.factory.ts b/apps/browser/src/background/service_factories/event-service.factory.ts new file mode 100644 index 000000000000..de7017b313e9 --- /dev/null +++ b/apps/browser/src/background/service_factories/event-service.factory.ts @@ -0,0 +1,45 @@ +import { EventService as AbstractEventService } from "@bitwarden/common/abstractions/event.service"; +import { EventService } from "@bitwarden/common/services/event.service"; +import { NoopEventService } from "@bitwarden/common/services/noopEvent.service"; + +import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory"; +import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory"; +import { CachedServices, factory, FactoryOptions } from "./factory-options"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { + organizationServiceFactory, + OrganizationServiceInitOptions, +} from "./organization-service.factory"; +import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; + +type EventServiceFactoryOptions = FactoryOptions & { + eventServiceOptions: { + useNoopService: boolean; + }; +}; + +export type EventServiceInitOptions = EventServiceFactoryOptions & + ApiServiceInitOptions & + CipherServiceInitOptions & + StateServiceInitOptions & + LogServiceInitOptions & + OrganizationServiceInitOptions; + +export function eventServiceFactory( + cache: { eventService?: AbstractEventService } & CachedServices, + opts: EventServiceInitOptions +): Promise { + return factory(cache, "eventService", opts, async () => { + if (opts.eventServiceOptions.useNoopService) { + return new NoopEventService(); + } + + return new EventService( + await apiServiceFactory(cache, opts), + await cipherServiceFactory(cache, opts), + await stateServiceFactory(cache, opts), + await logServiceFactory(cache, opts), + await organizationServiceFactory(cache, opts) + ); + }); +} diff --git a/apps/browser/src/background/service_factories/password-generation-service.factory.ts b/apps/browser/src/background/service_factories/password-generation-service.factory.ts new file mode 100644 index 000000000000..d69f22401c82 --- /dev/null +++ b/apps/browser/src/background/service_factories/password-generation-service.factory.ts @@ -0,0 +1,31 @@ +import { PasswordGenerationService as AbstractPasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; +import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; + +import { cryptoServiceFactory, CryptoServiceInitOptions } from "./crypto-service.factory"; +import { CachedServices, factory, FactoryOptions } from "./factory-options"; +import { policyServiceFactory, PolicyServiceInitOptions } from "./policy-service.factory"; +import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; + +type PasswordGenerationServiceFactoryOptions = FactoryOptions; + +export type PasswordGenerationServiceInitOptions = PasswordGenerationServiceFactoryOptions & + CryptoServiceInitOptions & + PolicyServiceInitOptions & + StateServiceInitOptions; + +export function passwordGenerationServiceFactory( + cache: { passwordGenerationService?: AbstractPasswordGenerationService } & CachedServices, + opts: PasswordGenerationServiceInitOptions +): Promise { + return factory( + cache, + "passwordGenerationService", + opts, + async () => + new PasswordGenerationService( + await cryptoServiceFactory(cache, opts), + await policyServiceFactory(cache, opts), + await stateServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/background/service_factories/totp-service.factory.ts b/apps/browser/src/background/service_factories/totp-service.factory.ts new file mode 100644 index 000000000000..f9be35406437 --- /dev/null +++ b/apps/browser/src/background/service_factories/totp-service.factory.ts @@ -0,0 +1,31 @@ +import { TotpService as AbstractTotpService } from "@bitwarden/common/abstractions/totp.service"; +import { TotpService } from "@bitwarden/common/services/totp.service"; + +import { + cryptoFunctionServiceFactory, + CryptoFunctionServiceInitOptions, +} from "./crypto-function-service.factory"; +import { CachedServices, factory, FactoryOptions } from "./factory-options"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; + +type TotpServiceFactoryOptions = FactoryOptions; + +export type TotpServiceInitOptions = TotpServiceFactoryOptions & + CryptoFunctionServiceInitOptions & + LogServiceInitOptions; + +export function totpServiceFactory( + cache: { totpService?: AbstractTotpService } & CachedServices, + opts: TotpServiceInitOptions +): Promise { + return factory( + cache, + "totpService", + opts, + async () => + new TotpService( + await cryptoFunctionServiceFactory(cache, opts), + await logServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/browser/sendTabsMessage.ts b/apps/browser/src/browser/sendTabsMessage.ts new file mode 100644 index 000000000000..8f6638d01983 --- /dev/null +++ b/apps/browser/src/browser/sendTabsMessage.ts @@ -0,0 +1,9 @@ +import { TabMessage } from "src/types/tab-messages"; + +export const sendTabsMessage = ( + tabId: number, + message: TabMessage, + responseCallback?: (response: T) => void +) => { + chrome.tabs.sendMessage(tabId, message, responseCallback); +}; diff --git a/apps/browser/src/content/miscUtils.ts b/apps/browser/src/content/miscUtils.ts new file mode 100644 index 000000000000..ee2c6f9383c9 --- /dev/null +++ b/apps/browser/src/content/miscUtils.ts @@ -0,0 +1,24 @@ +import { TabMessage } from "src/types/tab-messages"; + +async function copyText(text: string) { + await window.navigator.clipboard.writeText(text); +} + +async function onMessageListener( + msg: TabMessage, + sender: chrome.runtime.MessageSender, + responseCallback: (response: unknown) => void +) { + switch (msg.command) { + case "copyText": + await copyText(msg.text); + responseCallback(null); + break; + case "clearClipboard": + await copyText("\u0000"); + break; + default: + } +} + +chrome.runtime.onMessage.addListener(onMessageListener); diff --git a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts index 73cdf767357f..9b488ca61168 100644 --- a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts +++ b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts @@ -21,8 +21,15 @@ export function browserSession>(constructor: TCto constructor(...args: any[]) { super(...args); - // Require state service to be injected - const stateService = args.find((arg) => arg instanceof StateService); + // Require state service to be injected or be stateservice + let stateService: StateService | undefined; + if (this instanceof StateService) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + stateService = this; + } else { + stateService = args.find((arg) => arg instanceof StateService); + } + if (!stateService) { throw new Error( `Cannot decorate ${constructor.name} with browserSession, Browser's StateService must be injected` diff --git a/apps/browser/src/globals.d.ts b/apps/browser/src/globals.d.ts index 7307f696ae9e..26569c985233 100644 --- a/apps/browser/src/globals.d.ts +++ b/apps/browser/src/globals.d.ts @@ -1,4 +1,130 @@ declare function escape(s: string): string; declare function unescape(s: string): string; -declare let opr: any; + +/** + * https://dev.opera.com/extensions/addons-api/ + */ +type OperaAddons = { + /** + * https://dev.opera.com/extensions/addons-api/#method-installextension + */ + installExtension: ( + id: string, + success_callback: () => void, + error_callback: (errorMessage: string) => void + ) => void; +}; + +type OperaEvent = { + addListener: (callback: (state: T) => void) => void; +}; + +/** + * https://dev.opera.com/extensions/sidebar-action-api/#type-colorarray + */ +type ColorArray = [number, number, number, number]; + +/** + * https://dev.opera.com/extensions/sidebar-action-api/#type-imagedatatype + */ +type ImageDataType = ImageData; + +/** + * https://dev.opera.com/extensions/sidebar-action-api/ + */ +type OperaSidebarAction = { + /** + * https://dev.opera.com/extensions/sidebar-action-api/#method-settitle + */ + setTitle: (details: { title: string; tabId?: number }) => void; + /** + * https://dev.opera.com/extensions/sidebar-action-api/#method-gettitle + */ + getTitle: (details: { tabId?: number }, callback: (result: string) => void) => void; + /** + * https://dev.opera.com/extensions/sidebar-action-api/#method-seticon + */ + setIcon: ( + details: { + imageData?: ImageDataType | { [size: number]: ImageDataType }; + path?: string | { [size: number]: string }; + tabId?: number; + }, + callback?: () => void + ) => void; + /** + * https://dev.opera.com/extensions/sidebar-action-api/#method-setpanel + */ + setPanel: (details: { tabId?: number; panel: string }) => void; + /** + * https://dev.opera.com/extensions/sidebar-action-api/#method-getpanel + */ + getPanel: (details: { tabId?: number }, callback: (result: string) => void) => void; + /** + * *Not supported on mac* + * + * https://dev.opera.com/extensions/sidebar-action-api/#method-setbadgetext + */ + setBadgeText: (details: { text: string; tabId?: number }) => void; + /** + * *Not supported on mac* + * + * https://dev.opera.com/extensions/sidebar-action-api/#method-getbadgetext + */ + getBadgeText: (details: { tabId?: number }, callback: (result: string) => void) => void; + /** + * *Not supported on mac* + * + * https://dev.opera.com/extensions/sidebar-action-api/#method-setbadgebackgroundcolor + */ + setBadgeBackgroundColor: (details: { color: ColorArray | string; tabId?: number }) => void; + /** + * *Not supported on mac* + * + * https://dev.opera.com/extensions/sidebar-action-api/#method-getbadgebackgroundcolor + */ + getBadgeBackgroundColor: ( + details: { tabId?: number }, + callback: (result: ColorArray) => void + ) => void; + /** + * *Not supported on mac* + * + * https://dev.opera.com/extensions/sidebar-action-api/#events-onfocus + */ + onFocus: OperaEvent; + /** + * *Not supported on mac* + * + * https://dev.opera.com/extensions/sidebar-action-api/#events-onblur + */ + onBlur: OperaEvent; +}; + +type Opera = { + addons: OperaAddons; + sidebarAction: OperaSidebarAction; +}; + +declare namespace chrome { + /** + * This is for firefoxes sidebar action and it is based on the opera one but with a few less methods + * + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction + */ + let sidebarAction: + | Omit< + OperaSidebarAction, + | "setBadgeText" + | "getBadgeText" + | "setBadgeBackgroundColor" + | "getBadgeBackgroundColor" + | "onFocus" + | "onBlur" + > + | undefined; +} + +declare let opr: Opera | undefined; +declare let opera: unknown | undefined; declare let safari: any; diff --git a/apps/browser/src/listeners/onAlarmListener.ts b/apps/browser/src/listeners/onAlarmListener.ts new file mode 100644 index 000000000000..67bdb3ea71d8 --- /dev/null +++ b/apps/browser/src/listeners/onAlarmListener.ts @@ -0,0 +1,15 @@ +import { sendTabsMessage } from "../browser/sendTabsMessage"; + +export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { + switch (alarm.name) { + case "clearClipboard": { + const tabs = await chrome.tabs.query({ + active: true, + }); + if (tabs && tabs.length > 0) { + sendTabsMessage(tabs[0].id, { command: "clearClipboard" }); + } + break; + } + } +}; diff --git a/apps/browser/src/listeners/onCommandListener.ts b/apps/browser/src/listeners/onCommandListener.ts index 2a33e91e5783..3fbdb9749ede 100644 --- a/apps/browser/src/listeners/onCommandListener.ts +++ b/apps/browser/src/listeners/onCommandListener.ts @@ -1,131 +1,101 @@ +import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { GlobalState } from "@bitwarden/common/models/domain/globalState"; -import { AuthService } from "@bitwarden/common/services/auth.service"; -import { CipherService } from "@bitwarden/common/services/cipher.service"; -import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; -import { EncryptService } from "@bitwarden/common/services/encrypt.service"; -import { NoopEventService } from "@bitwarden/common/services/noopEvent.service"; -import { SearchService } from "@bitwarden/common/services/search.service"; -import { SettingsService } from "@bitwarden/common/services/settings.service"; -import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; -import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service"; +import { + authServiceFactory, + AuthServiceInitOptions, +} from "../background/service_factories/auth-service.factory"; +import { + autofillServiceFactory, + AutofillServiceInitOptions, +} from "../background/service_factories/autofill-service.factory"; +import { cipherServiceFactory } from "../background/service_factories/cipher-service.factory"; +import { cryptoServiceFactory } from "../background/service_factories/crypto-service.factory"; +import { encryptServiceFactory } from "../background/service_factories/encrypt-service.factory"; +import { logServiceFactory } from "../background/service_factories/log-service.factory"; +import { + passwordGenerationServiceFactory, + PasswordGenerationServiceInitOptions, +} from "../background/service_factories/password-generation-service.factory"; +import { searchServiceFactory } from "../background/service_factories/search-service.factory"; +import { stateServiceFactory } from "../background/service_factories/state-service.factory"; +import { sendTabsMessage } from "../browser/sendTabsMessage"; import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand"; import { Account } from "../models/account"; -import { StateService as AbstractStateService } from "../services/abstractions/state.service"; -import AutofillService from "../services/autofill.service"; -import { BrowserCryptoService } from "../services/browserCrypto.service"; -import BrowserLocalStorageService from "../services/browserLocalStorage.service"; -import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; -import I18nService from "../services/i18n.service"; -import { KeyGenerationService } from "../services/keyGeneration.service"; -import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service"; -import { StateService } from "../services/state.service"; export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => { switch (command) { case "autofill_login": await doAutoFillLogin(tab); break; + case "generate_password": + await doGeneratePasswordToClipboard(tab); + break; + // No need to support this one yet since this is only for safari + // case "open_popup": + // break; + case "lock_vault": + break; } }; const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise => { - const logService = new ConsoleLogService(false); - - const cryptoFunctionService = new WebCryptoFunctionService(self); - - const storageService = new BrowserLocalStorageService(); - - const secureStorageService = new BrowserLocalStorageService(); - - const memoryStorageService = new LocalBackedSessionStorageService( - new EncryptService(cryptoFunctionService, logService, false), - new KeyGenerationService(cryptoFunctionService) - ); - const stateFactory = new StateFactory(GlobalState, Account); - const stateMigrationService = new StateMigrationService( - storageService, - secureStorageService, - stateFactory - ); - - const stateService: AbstractStateService = new StateService( - storageService, - secureStorageService, - memoryStorageService, // AbstractStorageService - logService, - stateMigrationService, - stateFactory - ); - - await stateService.init(); - - const platformUtils = new BrowserPlatformUtilsService( - null, // MessagingService - null, // clipboardWriteCallback - null // biometricCallback - ); - - const cryptoService = new BrowserCryptoService( - cryptoFunctionService, - null, // AbstractEncryptService - platformUtils, - logService, - stateService - ); - - const settingsService = new SettingsService(stateService); - - const i18nService = new I18nService(chrome.i18n.getUILanguage()); - - await i18nService.init(); - - // Don't love this pt.1 - let searchService: SearchService = null; - - const cipherService = new CipherService( - cryptoService, - settingsService, - null, // ApiService - null, // FileUploadService, - i18nService, - () => searchService, // Don't love this pt.2 - logService, - stateService - ); - - // Don't love this pt.3 - searchService = new SearchService(cipherService, logService, i18nService); - - // TODO: Remove this before we encourage anyone to start using this - const eventService = new NoopEventService(); - - const autofillService = new AutofillService( - cipherService, - stateService, - null, // TotpService - eventService, - logService - ); - - const authService = new AuthService( - cryptoService, // CryptoService - null, // ApiService - null, // TokenService - null, // AppIdService - platformUtils, - null, // MessagingService - logService, - null, // KeyConnectorService - null, // EnvironmentService - stateService, - null, // TwoFactorService - i18nService - ); + const cache = {}; + const opts: AutofillServiceInitOptions & AuthServiceInitOptions = { + apiServiceOptions: { + logoutCallback: (_expired) => Promise.resolve(), + }, + cryptoFunctionServiceOptions: { + win: self, + }, + encryptServiceOptions: { + logMacFailures: false, + }, + eventServiceOptions: { + useNoopService: true, // Eventually get rid of this + }, + i18nServiceOptions: { + systemLanguage: chrome.i18n.getUILanguage(), + }, + logServiceOptions: { + isDev: false, + }, + platformUtilsServiceOptions: { + biometricCallback: () => Promise.resolve(true), + clipboardWriteCallback: (_clipboardValue, _clearMs) => Promise.resolve(), + win: self, + }, + stateMigrationServiceOptions: { + stateFactory: stateFactory, + }, + stateServiceOptions: { + stateFactory: stateFactory, + }, + keyConnectorServiceOptions: { + logoutCallback: (_expired, _userId) => Promise.resolve(), + }, + }; + + const cryptoService = await cryptoServiceFactory(cache, opts); + const encryptService = await encryptServiceFactory(cache, opts); + + self.bitwardenContainerService = { + getCryptoService: () => cryptoService, + getEncryptService: () => encryptService, + }; + + const stateService = await stateServiceFactory(cache, opts); + + const user = await stateService.getUserId(); + await stateService.setActiveUser(user); + + // Build required services + const logService = await logServiceFactory(cache, opts); + const authService = await authServiceFactory(cache, opts); const authStatus = await authService.getAuthStatus(); if (authStatus < AuthenticationStatus.Unlocked) { @@ -134,6 +104,70 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise => { return; } + // Create pointer for search service + let searchService: SearchService = null; + + // This sets up cipherService to be created with a pointer to the local searchService + await cipherServiceFactory(cache, { + ...opts, + cipherServiceOptions: { + searchServiceFactory: () => searchService, + }, + }); + + // Fill that pointer with a real object + searchService = await searchServiceFactory(cache, opts); + + // Continue making servies + const autofillService = await autofillServiceFactory(cache, opts); + const command = new AutoFillActiveTabCommand(autofillService); await command.doAutoFillActiveTabCommand(tab); }; + +const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise => { + const stateFactory = new StateFactory(GlobalState, Account); + + const cache = {}; + + const options: PasswordGenerationServiceInitOptions = { + cryptoFunctionServiceOptions: { + win: self, + }, + encryptServiceOptions: { + logMacFailures: false, + }, + logServiceOptions: { + isDev: false, + }, + platformUtilsServiceOptions: { + biometricCallback: () => Promise.resolve(true), + clipboardWriteCallback: (_clipboardValue, _clearMs) => Promise.resolve(), + win: self, + }, + stateMigrationServiceOptions: { + stateFactory: stateFactory, + }, + stateServiceOptions: { + stateFactory: stateFactory, + }, + }; + + const passwordGenerationService = await passwordGenerationServiceFactory(cache, options); + + const [passwordGenerationOptions] = await passwordGenerationService.getOptions(); + const password = await passwordGenerationService.generatePassword(passwordGenerationOptions); + sendTabsMessage(tab.id, { + command: "copyText", + text: password, + }); + + const stateService = await stateServiceFactory(cache, options); + const clearClipboard = await stateService.getClearClipboard(); + + if (clearClipboard != null) { + chrome.alarms.create("clearClipboard", { + when: Date.now() + clearClipboard * 1000, + }); + } +}; diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 68a9ff30c88c..7a3159bab9db 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -38,6 +38,12 @@ "css": ["content/autofill.css"], "matches": ["http://*/*", "https://*/*", "file:///*"], "run_at": "document_end" + }, + { + "all_frames": true, + "js": ["content/miscUtils.js"], + "matches": ["http://*/*", "https://*/*", "file:///*"], + "run_at": "document_end" } ], "background": { @@ -59,7 +65,8 @@ "unlimitedStorage", "clipboardRead", "clipboardWrite", - "idle" + "idle", + "alarms" ], "optional_permissions": ["nativeMessaging"], "host_permissions": ["http://*/*", "https://*/*"], diff --git a/apps/browser/src/services/browserPlatformUtils.service.ts b/apps/browser/src/services/browserPlatformUtils.service.ts index 5e31d64dc038..cc757c3f2464 100644 --- a/apps/browser/src/services/browserPlatformUtils.service.ts +++ b/apps/browser/src/services/browserPlatformUtils.service.ts @@ -33,8 +33,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService ) { this.deviceCache = DeviceType.FirefoxExtension; } else if ( - (!!(window as any).opr && !!opr.addons) || - !!(window as any).opera || + (typeof opr !== "undefined" && typeof opr.addons !== "undefined") || + typeof opera !== "undefined" || navigator.userAgent.indexOf(" OPR/") >= 0 ) { this.deviceCache = DeviceType.OperaExtension; @@ -42,7 +42,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService this.deviceCache = DeviceType.EdgeExtension; } else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) { this.deviceCache = DeviceType.VivaldiExtension; - } else if ((window as any).chrome && navigator.userAgent.indexOf(" Chrome/") !== -1) { + } else if (chrome && navigator.userAgent.indexOf(" Chrome/") !== -1) { this.deviceCache = DeviceType.ChromeExtension; } else if (navigator.userAgent.indexOf(" Safari/") !== -1) { this.deviceCache = DeviceType.SafariExtension; @@ -335,7 +335,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService } sidebarViewName(): string { - if ((window as any).chrome.sidebarAction && this.isFirefox()) { + if (window.chrome.sidebarAction && this.isFirefox()) { return "sidebar"; } else if (this.isOpera() && typeof opr !== "undefined" && opr.sidebarAction) { return "sidebar_panel"; diff --git a/apps/browser/src/types/tab-messages.ts b/apps/browser/src/types/tab-messages.ts new file mode 100644 index 000000000000..12496f5aa3d0 --- /dev/null +++ b/apps/browser/src/types/tab-messages.ts @@ -0,0 +1,9 @@ +export type TabMessage = CopyTextTabMessage | TabMessageBase<"clearClipboard">; + +export type TabMessageBase = { + command: T; +}; + +export type CopyTextTabMessage = TabMessageBase<"copyText"> & { + text: string; +}; diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 116ed19bd2e7..1ec45b69bb39 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -218,6 +218,8 @@ if (manifestVersion == 2) { return chunk.name === "background"; }, }; +} else { + config.entry["content/miscUtils"] = "./src/content/miscUtils.ts"; } module.exports = config; diff --git a/libs/common/src/abstractions/passwordGeneration.service.ts b/libs/common/src/abstractions/passwordGeneration.service.ts index 82bc021fb73d..ac27f266c399 100644 --- a/libs/common/src/abstractions/passwordGeneration.service.ts +++ b/libs/common/src/abstractions/passwordGeneration.service.ts @@ -1,20 +1,24 @@ import * as zxcvbn from "zxcvbn"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { PasswordGeneratorOptions } from "../models/domain/passwordGeneratorOptions"; import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions"; export abstract class PasswordGenerationService { - generatePassword: (options: any) => Promise; - generatePassphrase: (options: any) => Promise; - getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>; + generatePassword: (options: PasswordGeneratorOptions) => Promise; + generatePassphrase: (options: PasswordGeneratorOptions) => Promise; + getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; enforcePasswordGeneratorPoliciesOnOptions: ( - options: any - ) => Promise<[any, PasswordGeneratorPolicyOptions]>; + options: PasswordGeneratorOptions + ) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; getPasswordGeneratorPolicyOptions: () => Promise; - saveOptions: (options: any) => Promise; + saveOptions: (options: PasswordGeneratorOptions) => Promise; getHistory: () => Promise; - addHistory: (password: string) => Promise; - clear: (userId?: string) => Promise; + addHistory: (password: string) => Promise; + clear: (userId?: string) => Promise; passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult; - normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void; + normalizeOptions: ( + options: PasswordGeneratorOptions, + enforcedPolicyOptions: PasswordGeneratorPolicyOptions + ) => void; } diff --git a/libs/common/src/models/domain/passwordGeneratorOptions.ts b/libs/common/src/models/domain/passwordGeneratorOptions.ts new file mode 100644 index 000000000000..cfb2efcdce45 --- /dev/null +++ b/libs/common/src/models/domain/passwordGeneratorOptions.ts @@ -0,0 +1,17 @@ +export type PasswordGeneratorOptions = { + length: number; + ambiguous: boolean; + uppercase: boolean; + minUppercase: number; + lowercase: boolean; + minLowercase: number; + number: boolean; + minNumber: number; + special: boolean; + minSpecial: number; + numWords: number; + wordSeparator: string; + capitalize: boolean; + includeNumber: boolean; + type: "password" | "passphrase"; +}; diff --git a/libs/common/src/services/passwordGeneration.service.ts b/libs/common/src/services/passwordGeneration.service.ts index cf2a2ab5bf5b..37fc7b810f50 100644 --- a/libs/common/src/services/passwordGeneration.service.ts +++ b/libs/common/src/services/passwordGeneration.service.ts @@ -8,10 +8,11 @@ import { PolicyType } from "../enums/policyType"; import { EEFLongWordList } from "../misc/wordlist"; import { EncString } from "../models/domain/encString"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { PasswordGeneratorOptions } from "../models/domain/passwordGeneratorOptions"; import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions"; import { Policy } from "../models/domain/policy"; -const DefaultOptions = { +const DefaultOptions: PasswordGeneratorOptions = { length: 14, ambiguous: false, number: true, @@ -38,7 +39,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr private stateService: StateService ) {} - async generatePassword(options: any): Promise { + async generatePassword(options: PasswordGeneratorOptions): Promise { // overload defaults with given options const o = Object.assign({}, DefaultOptions, options); @@ -144,7 +145,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return password; } - async generatePassphrase(options: any): Promise { + async generatePassphrase(options: PasswordGeneratorOptions): Promise { const o = Object.assign({}, DefaultOptions, options); if (o.numWords == null || o.numWords <= 2) { @@ -177,7 +178,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return wordList.join(o.wordSeparator); } - async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { + async getOptions(): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> { let options = await this.stateService.getPasswordGenerationOptions(); if (options == null) { options = Object.assign({}, DefaultOptions); @@ -191,8 +192,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } async enforcePasswordGeneratorPoliciesOnOptions( - options: any - ): Promise<[any, PasswordGeneratorPolicyOptions]> { + options: PasswordGeneratorOptions + ): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> { let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions(); if (enforcedPolicyOptions != null) { if (options.length < enforcedPolicyOptions.minLength) { @@ -335,7 +336,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return enforcedOptions; } - async saveOptions(options: any) { + async saveOptions(options: PasswordGeneratorOptions) { await this.stateService.setPasswordGenerationOptions(options); } @@ -358,7 +359,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr : new Array(); } - async addHistory(password: string): Promise { + async addHistory(password: string): Promise { // Cannot add new history if no key is available const hasKey = await this.cryptoService.hasKey(); if (!hasKey) { @@ -384,7 +385,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory); } - async clear(userId?: string): Promise { + async clear(userId?: string): Promise { await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId }); await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId }); } @@ -403,7 +404,10 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return result; } - normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) { + normalizeOptions( + options: PasswordGeneratorOptions, + enforcedPolicyOptions: PasswordGeneratorPolicyOptions + ) { options.minLowercase = 0; options.minUppercase = 0; From 3125a88082be5cc5c89e70c000f394bb7cb1518d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 21 Sep 2022 08:42:40 -0400 Subject: [PATCH 02/15] Add JSDoc --- apps/browser/src/globals.d.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/browser/src/globals.d.ts b/apps/browser/src/globals.d.ts index 26569c985233..e023c06c301b 100644 --- a/apps/browser/src/globals.d.ts +++ b/apps/browser/src/globals.d.ts @@ -2,11 +2,11 @@ declare function escape(s: string): string; declare function unescape(s: string): string; /** - * https://dev.opera.com/extensions/addons-api/ + * @link https://dev.opera.com/extensions/addons-api/ */ type OperaAddons = { /** - * https://dev.opera.com/extensions/addons-api/#method-installextension + * @link https://dev.opera.com/extensions/addons-api/#method-installextension */ installExtension: ( id: string, @@ -20,29 +20,29 @@ type OperaEvent = { }; /** - * https://dev.opera.com/extensions/sidebar-action-api/#type-colorarray + * @link https://dev.opera.com/extensions/sidebar-action-api/#type-colorarray */ type ColorArray = [number, number, number, number]; /** - * https://dev.opera.com/extensions/sidebar-action-api/#type-imagedatatype + * @link https://dev.opera.com/extensions/sidebar-action-api/#type-imagedatatype */ type ImageDataType = ImageData; /** - * https://dev.opera.com/extensions/sidebar-action-api/ + * @link https://dev.opera.com/extensions/sidebar-action-api/ */ type OperaSidebarAction = { /** - * https://dev.opera.com/extensions/sidebar-action-api/#method-settitle + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-settitle */ setTitle: (details: { title: string; tabId?: number }) => void; /** - * https://dev.opera.com/extensions/sidebar-action-api/#method-gettitle + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-gettitle */ getTitle: (details: { tabId?: number }, callback: (result: string) => void) => void; /** - * https://dev.opera.com/extensions/sidebar-action-api/#method-seticon + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-seticon */ setIcon: ( details: { @@ -53,35 +53,35 @@ type OperaSidebarAction = { callback?: () => void ) => void; /** - * https://dev.opera.com/extensions/sidebar-action-api/#method-setpanel + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-setpanel */ setPanel: (details: { tabId?: number; panel: string }) => void; /** - * https://dev.opera.com/extensions/sidebar-action-api/#method-getpanel + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-getpanel */ getPanel: (details: { tabId?: number }, callback: (result: string) => void) => void; /** * *Not supported on mac* * - * https://dev.opera.com/extensions/sidebar-action-api/#method-setbadgetext + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-setbadgetext */ setBadgeText: (details: { text: string; tabId?: number }) => void; /** * *Not supported on mac* * - * https://dev.opera.com/extensions/sidebar-action-api/#method-getbadgetext + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-getbadgetext */ getBadgeText: (details: { tabId?: number }, callback: (result: string) => void) => void; /** * *Not supported on mac* * - * https://dev.opera.com/extensions/sidebar-action-api/#method-setbadgebackgroundcolor + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-setbadgebackgroundcolor */ setBadgeBackgroundColor: (details: { color: ColorArray | string; tabId?: number }) => void; /** * *Not supported on mac* * - * https://dev.opera.com/extensions/sidebar-action-api/#method-getbadgebackgroundcolor + * @link https://dev.opera.com/extensions/sidebar-action-api/#method-getbadgebackgroundcolor */ getBadgeBackgroundColor: ( details: { tabId?: number }, @@ -90,13 +90,13 @@ type OperaSidebarAction = { /** * *Not supported on mac* * - * https://dev.opera.com/extensions/sidebar-action-api/#events-onfocus + * @link https://dev.opera.com/extensions/sidebar-action-api/#events-onfocus */ onFocus: OperaEvent; /** * *Not supported on mac* * - * https://dev.opera.com/extensions/sidebar-action-api/#events-onblur + * @link https://dev.opera.com/extensions/sidebar-action-api/#events-onblur */ onBlur: OperaEvent; }; @@ -110,7 +110,7 @@ declare namespace chrome { /** * This is for firefoxes sidebar action and it is based on the opera one but with a few less methods * - * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction + * @link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sidebarAction */ let sidebarAction: | Omit< From ff46db3a19994f1d4596df3693a7939a46020c8a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 21 Sep 2022 09:58:19 -0400 Subject: [PATCH 03/15] Minor improvements --- .../cipher-service.factory.ts | 32 +++++++++++-------- apps/browser/src/content/miscUtils.ts | 1 - .../browser-session.decorator.ts | 9 +----- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/apps/browser/src/background/service_factories/cipher-service.factory.ts b/apps/browser/src/background/service_factories/cipher-service.factory.ts index d449327010b6..03141f2c84f5 100644 --- a/apps/browser/src/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/background/service_factories/cipher-service.factory.ts @@ -33,18 +33,22 @@ export function cipherServiceFactory( cache: { cipherService?: AbstractCipherService } & CachedServices, opts: CipherServiceInitOptions ): Promise { - return factory(cache, "cipherService", opts, async () => { - return new CipherService( - await cryptoServiceFactory(cache, opts), - await settingsServiceFactory(cache, opts), - await apiServiceFactory(cache, opts), - await fileUploadServiceFactory(cache, opts), - await i18nServiceFactory(cache, opts), - opts.cipherServiceOptions?.searchServiceFactory === undefined - ? () => cache.searchService - : opts.cipherServiceOptions.searchServiceFactory, - await logServiceFactory(cache, opts), - await stateServiceFactory(cache, opts) - ); - }); + return factory( + cache, + "cipherService", + opts, + async () => + new CipherService( + await cryptoServiceFactory(cache, opts), + await settingsServiceFactory(cache, opts), + await apiServiceFactory(cache, opts), + await fileUploadServiceFactory(cache, opts), + await i18nServiceFactory(cache, opts), + opts.cipherServiceOptions?.searchServiceFactory === undefined + ? () => cache.searchService + : opts.cipherServiceOptions.searchServiceFactory, + await logServiceFactory(cache, opts), + await stateServiceFactory(cache, opts) + ) + ); } diff --git a/apps/browser/src/content/miscUtils.ts b/apps/browser/src/content/miscUtils.ts index ee2c6f9383c9..1cb6c34ef975 100644 --- a/apps/browser/src/content/miscUtils.ts +++ b/apps/browser/src/content/miscUtils.ts @@ -12,7 +12,6 @@ async function onMessageListener( switch (msg.command) { case "copyText": await copyText(msg.text); - responseCallback(null); break; case "clearClipboard": await copyText("\u0000"); diff --git a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts index 9b488ca61168..8f43db89e96e 100644 --- a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts +++ b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts @@ -22,14 +22,7 @@ export function browserSession>(constructor: TCto super(...args); // Require state service to be injected or be stateservice - let stateService: StateService | undefined; - if (this instanceof StateService) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - stateService = this; - } else { - stateService = args.find((arg) => arg instanceof StateService); - } - + const stateService = args.find((arg) => arg instanceof StateService); if (!stateService) { throw new Error( `Cannot decorate ${constructor.name} with browserSession, Browser's StateService must be injected` From 2f5e1c4426f95a1f0c9feba4161ebac5d5132648 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 21 Sep 2022 10:53:52 -0400 Subject: [PATCH 04/15] Remove unneeded comment --- .../session-sync-observable/browser-session.decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts index 8f43db89e96e..73cdf767357f 100644 --- a/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts +++ b/apps/browser/src/decorators/session-sync-observable/browser-session.decorator.ts @@ -21,7 +21,7 @@ export function browserSession>(constructor: TCto constructor(...args: any[]) { super(...args); - // Require state service to be injected or be stateservice + // Require state service to be injected const stateService = args.find((arg) => arg instanceof StateService); if (!stateService) { throw new Error( From fd1f8889edc895b228ea8a4f41279c32e6d1bad5 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:09:13 -0400 Subject: [PATCH 05/15] Make some properties optional --- .../src/models/domain/passwordGeneratorOptions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/common/src/models/domain/passwordGeneratorOptions.ts b/libs/common/src/models/domain/passwordGeneratorOptions.ts index cfb2efcdce45..91f6cb953fe7 100644 --- a/libs/common/src/models/domain/passwordGeneratorOptions.ts +++ b/libs/common/src/models/domain/passwordGeneratorOptions.ts @@ -1,14 +1,14 @@ export type PasswordGeneratorOptions = { length: number; - ambiguous: boolean; + ambiguous?: boolean; uppercase: boolean; - minUppercase: number; + minUppercase?: number; lowercase: boolean; - minLowercase: number; + minLowercase?: number; number: boolean; - minNumber: number; + minNumber?: number; special: boolean; - minSpecial: number; + minSpecial?: number; numWords: number; wordSeparator: string; capitalize: boolean; From c2ee54a266a6b3a8a1c6980ed3ee61c92071c816 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:53:51 -0400 Subject: [PATCH 06/15] Remove main.background.ts changes --- apps/browser/src/background/main.background.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 681b856c2b3c..971e86752203 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -111,8 +111,6 @@ import RuntimeBackground from "./runtime.background"; import TabsBackground from "./tabs.background"; import WebRequestBackground from "./webRequest.background"; -type CommonSidebarAction = typeof chrome.sidebarAction | OperaSidebarAction; - export default class MainBackground { messagingService: MessagingServiceAbstraction; storageService: AbstractStorageService; @@ -470,7 +468,7 @@ export default class MainBackground { ? null : typeof opr !== "undefined" && opr.sidebarAction ? opr.sidebarAction - : window.chrome.sidebarAction; + : (window as any).chrome.sidebarAction; // Background this.runtimeBackground = new RuntimeBackground( @@ -1002,11 +1000,7 @@ export default class MainBackground { }); } - private async actionSetIcon( - theAction: CommonSidebarAction | typeof chrome.browserAction, - suffix: string, - windowId?: number - ): Promise { + private async actionSetIcon(theAction: any, suffix: string, windowId?: number): Promise { if (!theAction || !theAction.setIcon) { return; } @@ -1052,7 +1046,7 @@ export default class MainBackground { return; } - if ("setBadgeText" in this.sidebarAction) { + if (this.sidebarAction.setBadgeText) { this.sidebarAction.setBadgeText({ text: text, tabId: tabId, From ce2cfbc2c8511fab16c2f6981d178eddccea5743 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:54:53 -0400 Subject: [PATCH 07/15] One more --- apps/browser/src/background/main.background.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 971e86752203..cf83b6e78d19 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -177,7 +177,7 @@ export default class MainBackground { private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; - private sidebarAction: CommonSidebarAction; + private sidebarAction: any; private buildingContextMenu: boolean; private menuOptionsLoaded: any[] = []; private syncTimeout: any; From 52623ea8d8bac7e8fb3af8fdaa87fd7ddc13501d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:57:43 -0400 Subject: [PATCH 08/15] Lint --- apps/browser/src/browser/sendTabsMessage.ts | 2 +- apps/browser/src/content/miscUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/browser/sendTabsMessage.ts b/apps/browser/src/browser/sendTabsMessage.ts index 8f6638d01983..729e1e37c8f8 100644 --- a/apps/browser/src/browser/sendTabsMessage.ts +++ b/apps/browser/src/browser/sendTabsMessage.ts @@ -1,4 +1,4 @@ -import { TabMessage } from "src/types/tab-messages"; +import { TabMessage } from "../types/tab-messages"; export const sendTabsMessage = ( tabId: number, diff --git a/apps/browser/src/content/miscUtils.ts b/apps/browser/src/content/miscUtils.ts index 1cb6c34ef975..94e2e0f7a09e 100644 --- a/apps/browser/src/content/miscUtils.ts +++ b/apps/browser/src/content/miscUtils.ts @@ -1,4 +1,4 @@ -import { TabMessage } from "src/types/tab-messages"; +import { TabMessage } from "../types/tab-messages"; async function copyText(text: string) { await window.navigator.clipboard.writeText(text); From 9ced98e40af945677bddc882d9426d3f0fff3bf6 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 29 Sep 2022 12:06:53 -0400 Subject: [PATCH 09/15] Make all but length optional --- .../models/domain/passwordGeneratorOptions.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/common/src/models/domain/passwordGeneratorOptions.ts b/libs/common/src/models/domain/passwordGeneratorOptions.ts index 91f6cb953fe7..30bc74613154 100644 --- a/libs/common/src/models/domain/passwordGeneratorOptions.ts +++ b/libs/common/src/models/domain/passwordGeneratorOptions.ts @@ -1,17 +1,17 @@ export type PasswordGeneratorOptions = { length: number; ambiguous?: boolean; - uppercase: boolean; + uppercase?: boolean; minUppercase?: number; - lowercase: boolean; + lowercase?: boolean; minLowercase?: number; - number: boolean; + number?: boolean; minNumber?: number; - special: boolean; + special?: boolean; minSpecial?: number; - numWords: number; - wordSeparator: string; - capitalize: boolean; - includeNumber: boolean; - type: "password" | "passphrase"; + numWords?: number; + wordSeparator?: string; + capitalize?: boolean; + includeNumber?: boolean; + type?: "password" | "passphrase"; }; From 8b25d77a0476c74a69c9a613f30568d0dc7792fb Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:11:02 -0400 Subject: [PATCH 10/15] Address PR feedback --- apps/browser/src/background.ts | 2 +- .../event-service.factory.ts | 11 +- .../content/{miscUtils.ts => misc-utils.ts} | 0 ...nAlarmListener.ts => on-alarm-listener.ts} | 0 .../src/listeners/onCommandListener.ts | 188 ++++++++++-------- apps/browser/src/manifest.v3.json | 2 +- apps/browser/webpack.config.js | 2 +- 7 files changed, 109 insertions(+), 96 deletions(-) rename apps/browser/src/content/{miscUtils.ts => misc-utils.ts} (100%) rename apps/browser/src/listeners/{onAlarmListener.ts => on-alarm-listener.ts} (100%) diff --git a/apps/browser/src/background.ts b/apps/browser/src/background.ts index b73c3647e8d5..f3d63a1d0646 100644 --- a/apps/browser/src/background.ts +++ b/apps/browser/src/background.ts @@ -1,5 +1,5 @@ import MainBackground from "./background/main.background"; -import { onAlarmListener } from "./listeners/onAlarmListener"; +import { onAlarmListener } from "./listeners/on-alarm-listener"; import { onCommandListener } from "./listeners/onCommandListener"; import { onInstallListener } from "./listeners/onInstallListener"; diff --git a/apps/browser/src/background/service_factories/event-service.factory.ts b/apps/browser/src/background/service_factories/event-service.factory.ts index de7017b313e9..eee8b4a4aade 100644 --- a/apps/browser/src/background/service_factories/event-service.factory.ts +++ b/apps/browser/src/background/service_factories/event-service.factory.ts @@ -1,6 +1,5 @@ import { EventService as AbstractEventService } from "@bitwarden/common/abstractions/event.service"; import { EventService } from "@bitwarden/common/services/event.service"; -import { NoopEventService } from "@bitwarden/common/services/noopEvent.service"; import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory"; import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory"; @@ -12,11 +11,7 @@ import { } from "./organization-service.factory"; import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; -type EventServiceFactoryOptions = FactoryOptions & { - eventServiceOptions: { - useNoopService: boolean; - }; -}; +type EventServiceFactoryOptions = FactoryOptions; export type EventServiceInitOptions = EventServiceFactoryOptions & ApiServiceInitOptions & @@ -30,10 +25,6 @@ export function eventServiceFactory( opts: EventServiceInitOptions ): Promise { return factory(cache, "eventService", opts, async () => { - if (opts.eventServiceOptions.useNoopService) { - return new NoopEventService(); - } - return new EventService( await apiServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), diff --git a/apps/browser/src/content/miscUtils.ts b/apps/browser/src/content/misc-utils.ts similarity index 100% rename from apps/browser/src/content/miscUtils.ts rename to apps/browser/src/content/misc-utils.ts diff --git a/apps/browser/src/listeners/onAlarmListener.ts b/apps/browser/src/listeners/on-alarm-listener.ts similarity index 100% rename from apps/browser/src/listeners/onAlarmListener.ts rename to apps/browser/src/listeners/on-alarm-listener.ts diff --git a/apps/browser/src/listeners/onCommandListener.ts b/apps/browser/src/listeners/onCommandListener.ts index 3fbdb9749ede..aaa2f4fbb09f 100644 --- a/apps/browser/src/listeners/onCommandListener.ts +++ b/apps/browser/src/listeners/onCommandListener.ts @@ -1,29 +1,33 @@ -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { GlobalState } from "@bitwarden/common/models/domain/globalState"; +import { AuthService } from "@bitwarden/common/services/auth.service"; +import { CipherService } from "@bitwarden/common/services/cipher.service"; +import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; +import { EncryptService } from "@bitwarden/common/services/encrypt.service"; +import { NoopEventService } from "@bitwarden/common/services/noopEvent.service"; +import { SearchService } from "@bitwarden/common/services/search.service"; +import { SettingsService } from "@bitwarden/common/services/settings.service"; +import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; +import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service"; -import { - authServiceFactory, - AuthServiceInitOptions, -} from "../background/service_factories/auth-service.factory"; -import { - autofillServiceFactory, - AutofillServiceInitOptions, -} from "../background/service_factories/autofill-service.factory"; -import { cipherServiceFactory } from "../background/service_factories/cipher-service.factory"; -import { cryptoServiceFactory } from "../background/service_factories/crypto-service.factory"; -import { encryptServiceFactory } from "../background/service_factories/encrypt-service.factory"; -import { logServiceFactory } from "../background/service_factories/log-service.factory"; import { passwordGenerationServiceFactory, PasswordGenerationServiceInitOptions, } from "../background/service_factories/password-generation-service.factory"; -import { searchServiceFactory } from "../background/service_factories/search-service.factory"; import { stateServiceFactory } from "../background/service_factories/state-service.factory"; import { sendTabsMessage } from "../browser/sendTabsMessage"; import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand"; import { Account } from "../models/account"; +import { StateService as AbstractStateService } from "../services/abstractions/state.service"; +import AutofillService from "../services/autofill.service"; +import { BrowserCryptoService } from "../services/browserCrypto.service"; +import BrowserLocalStorageService from "../services/browserLocalStorage.service"; +import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service"; +import I18nService from "../services/i18n.service"; +import { KeyGenerationService } from "../services/keyGeneration.service"; +import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service"; +import { StateService } from "../services/state.service"; export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => { switch (command) { @@ -33,69 +37,104 @@ export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) = case "generate_password": await doGeneratePasswordToClipboard(tab); break; - // No need to support this one yet since this is only for safari - // case "open_popup": - // break; - case "lock_vault": - break; } }; const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise => { + const logService = new ConsoleLogService(false); + + const cryptoFunctionService = new WebCryptoFunctionService(self); + + const storageService = new BrowserLocalStorageService(); + + const secureStorageService = new BrowserLocalStorageService(); + + const memoryStorageService = new LocalBackedSessionStorageService( + new EncryptService(cryptoFunctionService, logService, false), + new KeyGenerationService(cryptoFunctionService) + ); + const stateFactory = new StateFactory(GlobalState, Account); - const cache = {}; - const opts: AutofillServiceInitOptions & AuthServiceInitOptions = { - apiServiceOptions: { - logoutCallback: (_expired) => Promise.resolve(), - }, - cryptoFunctionServiceOptions: { - win: self, - }, - encryptServiceOptions: { - logMacFailures: false, - }, - eventServiceOptions: { - useNoopService: true, // Eventually get rid of this - }, - i18nServiceOptions: { - systemLanguage: chrome.i18n.getUILanguage(), - }, - logServiceOptions: { - isDev: false, - }, - platformUtilsServiceOptions: { - biometricCallback: () => Promise.resolve(true), - clipboardWriteCallback: (_clipboardValue, _clearMs) => Promise.resolve(), - win: self, - }, - stateMigrationServiceOptions: { - stateFactory: stateFactory, - }, - stateServiceOptions: { - stateFactory: stateFactory, - }, - keyConnectorServiceOptions: { - logoutCallback: (_expired, _userId) => Promise.resolve(), - }, - }; + const stateMigrationService = new StateMigrationService( + storageService, + secureStorageService, + stateFactory + ); - const cryptoService = await cryptoServiceFactory(cache, opts); - const encryptService = await encryptServiceFactory(cache, opts); + const stateService: AbstractStateService = new StateService( + storageService, + secureStorageService, + memoryStorageService, // AbstractStorageService + logService, + stateMigrationService, + stateFactory + ); - self.bitwardenContainerService = { - getCryptoService: () => cryptoService, - getEncryptService: () => encryptService, - }; + await stateService.init(); + + const platformUtils = new BrowserPlatformUtilsService( + null, // MessagingService + null, // clipboardWriteCallback + null // biometricCallback + ); - const stateService = await stateServiceFactory(cache, opts); + const cryptoService = new BrowserCryptoService( + cryptoFunctionService, + null, // AbstractEncryptService + platformUtils, + logService, + stateService + ); - const user = await stateService.getUserId(); - await stateService.setActiveUser(user); + const settingsService = new SettingsService(stateService); - // Build required services - const logService = await logServiceFactory(cache, opts); - const authService = await authServiceFactory(cache, opts); + const i18nService = new I18nService(chrome.i18n.getUILanguage()); + + await i18nService.init(); + + // Don't love this pt.1 + let searchService: SearchService = null; + + const cipherService = new CipherService( + cryptoService, + settingsService, + null, // ApiService + null, // FileUploadService, + i18nService, + () => searchService, // Don't love this pt.2 + logService, + stateService + ); + + // Don't love this pt.3 + searchService = new SearchService(cipherService, logService, i18nService); + + // TODO: Remove this before we encourage anyone to start using this + const eventService = new NoopEventService(); + + const autofillService = new AutofillService( + cipherService, + stateService, + null, // TotpService + eventService, + logService + ); + + const authService = new AuthService( + cryptoService, // CryptoService + null, // ApiService + null, // TokenService + null, // AppIdService + platformUtils, + null, // MessagingService + logService, + null, // KeyConnectorService + null, // EnvironmentService + stateService, + null, // TwoFactorService + i18nService + ); const authStatus = await authService.getAuthStatus(); if (authStatus < AuthenticationStatus.Unlocked) { @@ -104,23 +143,6 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise => { return; } - // Create pointer for search service - let searchService: SearchService = null; - - // This sets up cipherService to be created with a pointer to the local searchService - await cipherServiceFactory(cache, { - ...opts, - cipherServiceOptions: { - searchServiceFactory: () => searchService, - }, - }); - - // Fill that pointer with a real object - searchService = await searchServiceFactory(cache, opts); - - // Continue making servies - const autofillService = await autofillServiceFactory(cache, opts); - const command = new AutoFillActiveTabCommand(autofillService); await command.doAutoFillActiveTabCommand(tab); }; diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 7a3159bab9db..a2efa2f97663 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -41,7 +41,7 @@ }, { "all_frames": true, - "js": ["content/miscUtils.js"], + "js": ["content/misc-utils.js"], "matches": ["http://*/*", "https://*/*", "file:///*"], "run_at": "document_end" } diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 1ec45b69bb39..9159028af01e 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -219,7 +219,7 @@ if (manifestVersion == 2) { }, }; } else { - config.entry["content/miscUtils"] = "./src/content/miscUtils.ts"; + config.entry["content/misc-utils"] = "./src/content/misc-utils.ts"; } module.exports = config; From 77aafa2f6fde1794ec3ac9167d81d90e1ab1fa44 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 4 Oct 2022 12:42:36 -0400 Subject: [PATCH 11/15] Move generate command code to command --- ...e-password-to-clipboard-command.factory.ts | 31 +++++++++++++++++++ .../src/commands/copy-to-clipboard-command.ts | 17 ++++++++++ .../generate-password-to-clipboard-command.ts | 26 ++++++++++++++++ .../src/listeners/onCommandListener.ts | 28 +++-------------- 4 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts create mode 100644 apps/browser/src/commands/copy-to-clipboard-command.ts create mode 100644 apps/browser/src/commands/generate-password-to-clipboard-command.ts diff --git a/apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts b/apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts new file mode 100644 index 000000000000..24c07350224f --- /dev/null +++ b/apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts @@ -0,0 +1,31 @@ +import { GeneratePasswordToClipboardCommand } from "../../commands/generate-password-to-clipboard-command"; + +import { factory, FactoryOptions } from "./factory-options"; +import { + passwordGenerationServiceFactory, + PasswordGenerationServiceInitOptions, +} from "./password-generation-service.factory"; +import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; + +type GeneratePasswordToClipboardCommandOptions = FactoryOptions; + +export type GeneratePasswordToClipboardCommandInitOptions = + GeneratePasswordToClipboardCommandOptions & + PasswordGenerationServiceInitOptions & + StateServiceInitOptions; + +export function generatePasswordToClipboardCommandFactory( + cache: { generatePasswordToClipboardCommand?: GeneratePasswordToClipboardCommand }, + opts: GeneratePasswordToClipboardCommandInitOptions +): Promise { + return factory( + cache, + "generatePasswordToClipboardCommand", + opts, + async () => + new GeneratePasswordToClipboardCommand( + await passwordGenerationServiceFactory(cache, opts), + await stateServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/commands/copy-to-clipboard-command.ts b/apps/browser/src/commands/copy-to-clipboard-command.ts new file mode 100644 index 000000000000..0ae498d4b081 --- /dev/null +++ b/apps/browser/src/commands/copy-to-clipboard-command.ts @@ -0,0 +1,17 @@ +import { sendTabsMessage } from "../browser/sendTabsMessage"; + +/** + * Copies text to the clipboard in a MV3 safe way. + * @param tab - The tab that the text will be sent to so that it can be copied to the users clipboard this needs to be an active tab or the DOM won't be able to be used to do the action. The tab sent in here should be from a user started action or queried for active tabs. + * @param text - The text that you want added to the users clipboard. + */ +export const copyToClipboard = async (tab: chrome.tabs.Tab, text: string) => { + if (tab.id == null) { + throw new Error("Cannot copy text to clipboard with a tab that does not have an id."); + } + + sendTabsMessage(tab.id, { + command: "copyText", + text: text, + }); +}; diff --git a/apps/browser/src/commands/generate-password-to-clipboard-command.ts b/apps/browser/src/commands/generate-password-to-clipboard-command.ts new file mode 100644 index 000000000000..eab9c361b53c --- /dev/null +++ b/apps/browser/src/commands/generate-password-to-clipboard-command.ts @@ -0,0 +1,26 @@ +import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; + +import { copyToClipboard } from "./copy-to-clipboard-command"; + +export class GeneratePasswordToClipboardCommand { + constructor( + private passwordGenerationService: PasswordGenerationService, + private stateService: StateService + ) {} + + async generatePasswordToClipboard(tab: chrome.tabs.Tab) { + const [options] = await this.passwordGenerationService.getOptions(); + const password = await this.passwordGenerationService.generatePassword(options); + + copyToClipboard(tab, password); + + const clearClipboard = await this.stateService.getClearClipboard(); + + if (clearClipboard != null) { + chrome.alarms.create("clearClipboard", { + when: Date.now() + clearClipboard * 1000, + }); + } + } +} diff --git a/apps/browser/src/listeners/onCommandListener.ts b/apps/browser/src/listeners/onCommandListener.ts index aaa2f4fbb09f..f1eff881f14d 100644 --- a/apps/browser/src/listeners/onCommandListener.ts +++ b/apps/browser/src/listeners/onCommandListener.ts @@ -11,12 +11,8 @@ import { SettingsService } from "@bitwarden/common/services/settings.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service"; -import { - passwordGenerationServiceFactory, - PasswordGenerationServiceInitOptions, -} from "../background/service_factories/password-generation-service.factory"; -import { stateServiceFactory } from "../background/service_factories/state-service.factory"; -import { sendTabsMessage } from "../browser/sendTabsMessage"; +import { generatePasswordToClipboardCommandFactory } from "../background/service_factories/generate-password-to-clipboard-command.factory"; +import { PasswordGenerationServiceInitOptions } from "../background/service_factories/password-generation-service.factory"; import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand"; import { Account } from "../models/account"; import { StateService as AbstractStateService } from "../services/abstractions/state.service"; @@ -151,7 +147,6 @@ const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise Date: Thu, 6 Oct 2022 09:55:14 -0400 Subject: [PATCH 12/15] Address PR feedback --- ...e-password-to-clipboard-command.factory.ts | 31 ------------------- apps/browser/src/browser/browserApi.ts | 10 ++++++ apps/browser/src/browser/sendTabsMessage.ts | 9 ------ .../src/commands/copy-to-clipboard-command.ts | 4 +-- .../src/listeners/on-alarm-listener.ts | 4 +-- .../src/listeners/onCommandListener.ts | 13 ++++++-- 6 files changed, 24 insertions(+), 47 deletions(-) delete mode 100644 apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts delete mode 100644 apps/browser/src/browser/sendTabsMessage.ts diff --git a/apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts b/apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts deleted file mode 100644 index 24c07350224f..000000000000 --- a/apps/browser/src/background/service_factories/generate-password-to-clipboard-command.factory.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GeneratePasswordToClipboardCommand } from "../../commands/generate-password-to-clipboard-command"; - -import { factory, FactoryOptions } from "./factory-options"; -import { - passwordGenerationServiceFactory, - PasswordGenerationServiceInitOptions, -} from "./password-generation-service.factory"; -import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; - -type GeneratePasswordToClipboardCommandOptions = FactoryOptions; - -export type GeneratePasswordToClipboardCommandInitOptions = - GeneratePasswordToClipboardCommandOptions & - PasswordGenerationServiceInitOptions & - StateServiceInitOptions; - -export function generatePasswordToClipboardCommandFactory( - cache: { generatePasswordToClipboardCommand?: GeneratePasswordToClipboardCommand }, - opts: GeneratePasswordToClipboardCommandInitOptions -): Promise { - return factory( - cache, - "generatePasswordToClipboardCommand", - opts, - async () => - new GeneratePasswordToClipboardCommand( - await passwordGenerationServiceFactory(cache, opts), - await stateServiceFactory(cache, opts) - ) - ); -} diff --git a/apps/browser/src/browser/browserApi.ts b/apps/browser/src/browser/browserApi.ts index 7b0152a9bdb1..363adb43750e 100644 --- a/apps/browser/src/browser/browserApi.ts +++ b/apps/browser/src/browser/browserApi.ts @@ -1,3 +1,5 @@ +import { TabMessage } from "../types/tab-messages"; + export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isSafariApi: boolean = @@ -80,6 +82,14 @@ export class BrowserApi { }); } + static sendTabsMessage( + tabId: number, + message: TabMessage, + responseCallback?: (response: T) => void + ) { + chrome.tabs.sendMessage(tabId, message, responseCallback); + } + static async getPrivateModeWindows(): Promise { return (await browser.windows.getAll()).filter((win) => win.incognito); } diff --git a/apps/browser/src/browser/sendTabsMessage.ts b/apps/browser/src/browser/sendTabsMessage.ts deleted file mode 100644 index 729e1e37c8f8..000000000000 --- a/apps/browser/src/browser/sendTabsMessage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TabMessage } from "../types/tab-messages"; - -export const sendTabsMessage = ( - tabId: number, - message: TabMessage, - responseCallback?: (response: T) => void -) => { - chrome.tabs.sendMessage(tabId, message, responseCallback); -}; diff --git a/apps/browser/src/commands/copy-to-clipboard-command.ts b/apps/browser/src/commands/copy-to-clipboard-command.ts index 0ae498d4b081..e1f2cca3f2cc 100644 --- a/apps/browser/src/commands/copy-to-clipboard-command.ts +++ b/apps/browser/src/commands/copy-to-clipboard-command.ts @@ -1,4 +1,4 @@ -import { sendTabsMessage } from "../browser/sendTabsMessage"; +import { BrowserApi } from "../browser/browserApi"; /** * Copies text to the clipboard in a MV3 safe way. @@ -10,7 +10,7 @@ export const copyToClipboard = async (tab: chrome.tabs.Tab, text: string) => { throw new Error("Cannot copy text to clipboard with a tab that does not have an id."); } - sendTabsMessage(tab.id, { + BrowserApi.sendTabsMessage(tab.id, { command: "copyText", text: text, }); diff --git a/apps/browser/src/listeners/on-alarm-listener.ts b/apps/browser/src/listeners/on-alarm-listener.ts index 67bdb3ea71d8..5ad483d33a06 100644 --- a/apps/browser/src/listeners/on-alarm-listener.ts +++ b/apps/browser/src/listeners/on-alarm-listener.ts @@ -1,4 +1,4 @@ -import { sendTabsMessage } from "../browser/sendTabsMessage"; +import { BrowserApi } from "../browser/browserApi"; export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { switch (alarm.name) { @@ -7,7 +7,7 @@ export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { active: true, }); if (tabs && tabs.length > 0) { - sendTabsMessage(tabs[0].id, { command: "clearClipboard" }); + BrowserApi.sendTabsMessage(tabs[0].id, { command: "clearClipboard" }); } break; } diff --git a/apps/browser/src/listeners/onCommandListener.ts b/apps/browser/src/listeners/onCommandListener.ts index f1eff881f14d..07ebf4be3061 100644 --- a/apps/browser/src/listeners/onCommandListener.ts +++ b/apps/browser/src/listeners/onCommandListener.ts @@ -11,9 +11,13 @@ import { SettingsService } from "@bitwarden/common/services/settings.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service"; -import { generatePasswordToClipboardCommandFactory } from "../background/service_factories/generate-password-to-clipboard-command.factory"; -import { PasswordGenerationServiceInitOptions } from "../background/service_factories/password-generation-service.factory"; +import { + passwordGenerationServiceFactory, + PasswordGenerationServiceInitOptions, +} from "../background/service_factories/password-generation-service.factory"; +import { stateServiceFactory } from "../background/service_factories/state-service.factory"; import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand"; +import { GeneratePasswordToClipboardCommand } from "../commands/generate-password-to-clipboard-command"; import { Account } from "../models/account"; import { StateService as AbstractStateService } from "../services/abstractions/state.service"; import AutofillService from "../services/autofill.service"; @@ -170,6 +174,9 @@ const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise Date: Mon, 10 Oct 2022 10:32:55 -0400 Subject: [PATCH 13/15] Use new alarm scheme --- apps/browser/src/background.ts | 15 +++- ...rate-password-to-clipboard-command.spec.ts | 73 +++++++++++++++++++ .../generate-password-to-clipboard-command.ts | 7 +- .../src/listeners/clearClipboard.spec.ts | 68 +++++++++++++++++ apps/browser/src/listeners/clearClipboard.ts | 48 ++++++++++++ .../src/listeners/on-alarm-listener.ts | 15 ---- .../services/abstractions/state.service.ts | 2 + apps/browser/src/services/state.service.ts | 8 ++ 8 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts create mode 100644 apps/browser/src/listeners/clearClipboard.spec.ts create mode 100644 apps/browser/src/listeners/clearClipboard.ts delete mode 100644 apps/browser/src/listeners/on-alarm-listener.ts diff --git a/apps/browser/src/background.ts b/apps/browser/src/background.ts index f3d63a1d0646..45fc6f10d902 100644 --- a/apps/browser/src/background.ts +++ b/apps/browser/src/background.ts @@ -1,14 +1,25 @@ import MainBackground from "./background/main.background"; -import { onAlarmListener } from "./listeners/on-alarm-listener"; +import { ClearClipboard } from "./listeners/clearClipboard"; import { onCommandListener } from "./listeners/onCommandListener"; import { onInstallListener } from "./listeners/onInstallListener"; +type AlarmAction = (executionTime: Date, serviceCache: Record) => void; + +const AlarmActions: AlarmAction[] = [ClearClipboard.run]; + const manifest = chrome.runtime.getManifest(); if (manifest.manifest_version === 3) { chrome.commands.onCommand.addListener(onCommandListener); chrome.runtime.onInstalled.addListener(onInstallListener); - chrome.alarms.onAlarm.addListener(onAlarmListener); + chrome.alarms.onAlarm.addListener((_alarm) => { + const executionTime = new Date(); + const serviceCache = {}; + + for (const alarmAction of AlarmActions) { + alarmAction(executionTime, serviceCache); + } + }); } else { const bitwardenMain = ((window as any).bitwardenMain = new MainBackground()); bitwardenMain.bootstrap().then(() => { diff --git a/apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts new file mode 100644 index 000000000000..f7db450a08b7 --- /dev/null +++ b/apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts @@ -0,0 +1,73 @@ +import { matches, mock, MockProxy } from "jest-mock-extended"; + +import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; + +import { BrowserApi } from "../browser/browserApi"; +import { StateService } from "../services/abstractions/state.service"; + +import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; + +describe("GeneratePasswordToClipboardCommand", () => { + let passwordGenerationService: MockProxy; + let stateService: MockProxy; + + let sut: GeneratePasswordToClipboardCommand; + + beforeEach(() => { + passwordGenerationService = mock(); + stateService = mock(); + + passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]); + + passwordGenerationService.generatePassword.mockResolvedValue("PASSWORD"); + + jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue(); + + sut = new GeneratePasswordToClipboardCommand(passwordGenerationService, stateService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("generatePasswordToClipboard", () => { + it("has clear clipboard value", async () => { + stateService.getClearClipboard.mockResolvedValue(5 * 60); // 5 minutes + + await sut.generatePasswordToClipboard({ id: 1 } as any); + + expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); + + expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { + command: "copyText", + text: "PASSWORD", + }); + + expect(stateService.setClearClipboardTime).toHaveBeenCalledTimes(1); + + expect(stateService.setClearClipboardTime).toHaveBeenCalledWith( + matches((time: number) => { + const now = new Date(); + const date = new Date(time); + + return date.getMinutes() - now.getMinutes() === 5; + }) + ); + }); + + it("does not have clear clipboard value", async () => { + stateService.getClearClipboard.mockResolvedValue(null); + + await sut.generatePasswordToClipboard({ id: 1 } as any); + + expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); + + expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { + command: "copyText", + text: "PASSWORD", + }); + + expect(stateService.setClearClipboardTime).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/commands/generate-password-to-clipboard-command.ts b/apps/browser/src/commands/generate-password-to-clipboard-command.ts index eab9c361b53c..3f14412201a4 100644 --- a/apps/browser/src/commands/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/commands/generate-password-to-clipboard-command.ts @@ -1,5 +1,6 @@ import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; -import { StateService } from "@bitwarden/common/abstractions/state.service"; + +import { StateService } from "../services/abstractions/state.service"; import { copyToClipboard } from "./copy-to-clipboard-command"; @@ -18,9 +19,7 @@ export class GeneratePasswordToClipboardCommand { const clearClipboard = await this.stateService.getClearClipboard(); if (clearClipboard != null) { - chrome.alarms.create("clearClipboard", { - when: Date.now() + clearClipboard * 1000, - }); + await this.stateService.setClearClipboardTime(Date.now() + clearClipboard * 1000); } } } diff --git a/apps/browser/src/listeners/clearClipboard.spec.ts b/apps/browser/src/listeners/clearClipboard.spec.ts new file mode 100644 index 000000000000..a256837f445d --- /dev/null +++ b/apps/browser/src/listeners/clearClipboard.spec.ts @@ -0,0 +1,68 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { BrowserApi } from "../browser/browserApi"; +import { StateService } from "../services/abstractions/state.service"; + +import { ClearClipboard } from "./clearClipboard"; + +describe("clearClipboard", () => { + describe("run", () => { + let stateService: MockProxy; + let serviceCache: Record; + + beforeEach(() => { + stateService = mock(); + serviceCache = { + stateService: stateService, + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("has a clear time that is past execution time", async () => { + const executionTime = new Date(2022, 1, 1, 12); + const clearTime = new Date(2022, 1, 1, 12, 1); + + jest.spyOn(BrowserApi, "getActiveTabs").mockResolvedValue([ + { + id: 1, + }, + ] as any); + + jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue(); + + stateService.getClearClipboardTime.mockResolvedValue(clearTime.getTime()); + + await ClearClipboard.run(executionTime, serviceCache); + + expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); + + expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { + command: "clearClipboard", + }); + }); + + it("has a clear time before execution time", async () => { + const executionTime = new Date(2022, 1, 1, 12); + const clearTime = new Date(2022, 1, 1, 11); + + stateService.getClearClipboardTime.mockResolvedValue(clearTime.getTime()); + + await ClearClipboard.run(executionTime, serviceCache); + + expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled(); + }); + + it("has an undefined clearTime", async () => { + const executionTime = new Date(2022, 1, 1); + + stateService.getClearClipboardTime.mockResolvedValue(undefined); + + await ClearClipboard.run(executionTime, serviceCache); + + expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/listeners/clearClipboard.ts b/apps/browser/src/listeners/clearClipboard.ts new file mode 100644 index 000000000000..28f9a0fb44b4 --- /dev/null +++ b/apps/browser/src/listeners/clearClipboard.ts @@ -0,0 +1,48 @@ +import { StateFactory } from "@bitwarden/common/factories/stateFactory"; +import { GlobalState } from "@bitwarden/common/models/domain/globalState"; + +import { stateServiceFactory } from "../background/service_factories/state-service.factory"; +import { BrowserApi } from "../browser/browserApi"; +import { Account } from "../models/account"; + +export class ClearClipboard { + static async run(executionTime: Date, serviceCache: Record) { + const stateFactory = new StateFactory(GlobalState, Account); + const stateService = await stateServiceFactory(serviceCache, { + cryptoFunctionServiceOptions: { + win: self, + }, + encryptServiceOptions: { + logMacFailures: false, + }, + logServiceOptions: { + isDev: false, + }, + stateMigrationServiceOptions: { + stateFactory: stateFactory, + }, + stateServiceOptions: { + stateFactory: stateFactory, + }, + }); + + const clearClipboardTime = await stateService.getClearClipboardTime(); + + if (!clearClipboardTime) { + return; + } + + if (clearClipboardTime < executionTime.getTime()) { + return; + } + + const activeTabs = await BrowserApi.getActiveTabs(); + if (!activeTabs || activeTabs.length === 0) { + return; + } + + BrowserApi.sendTabsMessage(activeTabs[0].id, { + command: "clearClipboard", + }); + } +} diff --git a/apps/browser/src/listeners/on-alarm-listener.ts b/apps/browser/src/listeners/on-alarm-listener.ts deleted file mode 100644 index 5ad483d33a06..000000000000 --- a/apps/browser/src/listeners/on-alarm-listener.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BrowserApi } from "../browser/browserApi"; - -export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { - switch (alarm.name) { - case "clearClipboard": { - const tabs = await chrome.tabs.query({ - active: true, - }); - if (tabs && tabs.length > 0) { - BrowserApi.sendTabsMessage(tabs[0].id, { command: "clearClipboard" }); - } - break; - } - } -}; diff --git a/apps/browser/src/services/abstractions/state.service.ts b/apps/browser/src/services/abstractions/state.service.ts index 0f552154d6a4..e0bdedbd4ebe 100644 --- a/apps/browser/src/services/abstractions/state.service.ts +++ b/apps/browser/src/services/abstractions/state.service.ts @@ -33,4 +33,6 @@ export abstract class StateService extends BaseStateServiceAbstraction value: BrowserComponentState, options?: StorageOptions ) => Promise; + getClearClipboardTime: () => Promise; + setClearClipboardTime: (time: number) => Promise; } diff --git a/apps/browser/src/services/state.service.ts b/apps/browser/src/services/state.service.ts index 78bc721031a0..f0b04188239b 100644 --- a/apps/browser/src/services/state.service.ts +++ b/apps/browser/src/services/state.service.ts @@ -129,4 +129,12 @@ export class StateService this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } + + async setClearClipboardTime(time: number): Promise { + this.setInSessionMemory("clearClipboardTime", time); + } + + async getClearClipboardTime(): Promise { + return this.getFromSessionMemory("clearClipboardTime"); + } } From 2718ca0627ce67d265d7eaeb10ef574439ecf29a Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 12 Oct 2022 15:22:21 -0400 Subject: [PATCH 14/15] Let feature handle state keys Moves to a feature folder and creates clipboard-module level state handler functions. StateService is being paired down to storage routing, so we are handling storage specifics in-module. Co-authored-by: Justin Baur Co-authored-by: Daniel Smith --- .../clearClipboard.spec.ts | 17 ++++++++++--- .../clearClipboard.ts | 4 ++- apps/browser/src/clipboard/clipboard-state.ts | 10 ++++++++ .../copy-to-clipboard-command.ts | 0 ...rate-password-to-clipboard-command.spec.ts | 25 +++++++++++-------- .../generate-password-to-clipboard-command.ts | 3 ++- apps/browser/src/clipboard/index.ts | 3 +++ .../services/abstractions/state.service.ts | 2 -- apps/browser/src/services/state.service.ts | 8 ------ 9 files changed, 46 insertions(+), 26 deletions(-) rename apps/browser/src/{listeners => clipboard}/clearClipboard.spec.ts (76%) rename apps/browser/src/{listeners => clipboard}/clearClipboard.ts (90%) create mode 100644 apps/browser/src/clipboard/clipboard-state.ts rename apps/browser/src/{commands => clipboard}/copy-to-clipboard-command.ts (100%) rename apps/browser/src/{commands => clipboard}/generate-password-to-clipboard-command.spec.ts (78%) rename apps/browser/src/{commands => clipboard}/generate-password-to-clipboard-command.ts (84%) create mode 100644 apps/browser/src/clipboard/index.ts diff --git a/apps/browser/src/listeners/clearClipboard.spec.ts b/apps/browser/src/clipboard/clearClipboard.spec.ts similarity index 76% rename from apps/browser/src/listeners/clearClipboard.spec.ts rename to apps/browser/src/clipboard/clearClipboard.spec.ts index a256837f445d..3e7d4e36712d 100644 --- a/apps/browser/src/listeners/clearClipboard.spec.ts +++ b/apps/browser/src/clipboard/clearClipboard.spec.ts @@ -4,6 +4,17 @@ import { BrowserApi } from "../browser/browserApi"; import { StateService } from "../services/abstractions/state.service"; import { ClearClipboard } from "./clearClipboard"; +import { getClearClipboardTime, setClearClipboardTime } from "./clipboard-state"; + +jest.mock("./clipboard-state", () => { + return { + getClearClipboardTime: jest.fn(), + setClearClipboardTime: jest.fn(), + }; +}); + +const getClearClipboardTimeMock = getClearClipboardTime as jest.Mock; +const setClearClipboardTimeMock = setClearClipboardTime as jest.Mock; describe("clearClipboard", () => { describe("run", () => { @@ -33,7 +44,7 @@ describe("clearClipboard", () => { jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue(); - stateService.getClearClipboardTime.mockResolvedValue(clearTime.getTime()); + getClearClipboardTimeMock.mockResolvedValue(clearTime.getTime()); await ClearClipboard.run(executionTime, serviceCache); @@ -48,7 +59,7 @@ describe("clearClipboard", () => { const executionTime = new Date(2022, 1, 1, 12); const clearTime = new Date(2022, 1, 1, 11); - stateService.getClearClipboardTime.mockResolvedValue(clearTime.getTime()); + setClearClipboardTimeMock.mockResolvedValue(clearTime.getTime()); await ClearClipboard.run(executionTime, serviceCache); @@ -58,7 +69,7 @@ describe("clearClipboard", () => { it("has an undefined clearTime", async () => { const executionTime = new Date(2022, 1, 1); - stateService.getClearClipboardTime.mockResolvedValue(undefined); + getClearClipboardTimeMock.mockResolvedValue(undefined); await ClearClipboard.run(executionTime, serviceCache); diff --git a/apps/browser/src/listeners/clearClipboard.ts b/apps/browser/src/clipboard/clearClipboard.ts similarity index 90% rename from apps/browser/src/listeners/clearClipboard.ts rename to apps/browser/src/clipboard/clearClipboard.ts index 28f9a0fb44b4..8a40f2d10341 100644 --- a/apps/browser/src/listeners/clearClipboard.ts +++ b/apps/browser/src/clipboard/clearClipboard.ts @@ -5,6 +5,8 @@ import { stateServiceFactory } from "../background/service_factories/state-servi import { BrowserApi } from "../browser/browserApi"; import { Account } from "../models/account"; +import { getClearClipboardTime } from "./clipboard-state"; + export class ClearClipboard { static async run(executionTime: Date, serviceCache: Record) { const stateFactory = new StateFactory(GlobalState, Account); @@ -26,7 +28,7 @@ export class ClearClipboard { }, }); - const clearClipboardTime = await stateService.getClearClipboardTime(); + const clearClipboardTime = await getClearClipboardTime(stateService); if (!clearClipboardTime) { return; diff --git a/apps/browser/src/clipboard/clipboard-state.ts b/apps/browser/src/clipboard/clipboard-state.ts new file mode 100644 index 000000000000..a1c15addc0a1 --- /dev/null +++ b/apps/browser/src/clipboard/clipboard-state.ts @@ -0,0 +1,10 @@ +import { StateService } from "../services/abstractions/state.service"; + +const clearClipboardStorageKey = "clearClipboardTime"; +export const getClearClipboardTime = async (stateService: StateService) => { + return await stateService.getFromSessionMemory(clearClipboardStorageKey); +}; + +export const setClearClipboardTime = async (stateService: StateService, time: number) => { + await stateService.setInSessionMemory(clearClipboardStorageKey, time); +}; diff --git a/apps/browser/src/commands/copy-to-clipboard-command.ts b/apps/browser/src/clipboard/copy-to-clipboard-command.ts similarity index 100% rename from apps/browser/src/commands/copy-to-clipboard-command.ts rename to apps/browser/src/clipboard/copy-to-clipboard-command.ts diff --git a/apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts similarity index 78% rename from apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts rename to apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts index f7db450a08b7..e9c2141211ff 100644 --- a/apps/browser/src/commands/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/clipboard/generate-password-to-clipboard-command.spec.ts @@ -1,12 +1,22 @@ -import { matches, mock, MockProxy } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { BrowserApi } from "../browser/browserApi"; import { StateService } from "../services/abstractions/state.service"; +import { setClearClipboardTime } from "./clipboard-state"; import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; +jest.mock("./clipboard-state", () => { + return { + getClearClipboardTime: jest.fn(), + setClearClipboardTime: jest.fn(), + }; +}); + +const setClearClipboardTimeMock = setClearClipboardTime as jest.Mock; + describe("GeneratePasswordToClipboardCommand", () => { let passwordGenerationService: MockProxy; let stateService: MockProxy; @@ -43,16 +53,9 @@ describe("GeneratePasswordToClipboardCommand", () => { text: "PASSWORD", }); - expect(stateService.setClearClipboardTime).toHaveBeenCalledTimes(1); - - expect(stateService.setClearClipboardTime).toHaveBeenCalledWith( - matches((time: number) => { - const now = new Date(); - const date = new Date(time); + expect(setClearClipboardTimeMock).toHaveBeenCalledTimes(1); - return date.getMinutes() - now.getMinutes() === 5; - }) - ); + expect(setClearClipboardTimeMock).toHaveBeenCalledWith(stateService, expect.any(Number)); }); it("does not have clear clipboard value", async () => { @@ -67,7 +70,7 @@ describe("GeneratePasswordToClipboardCommand", () => { text: "PASSWORD", }); - expect(stateService.setClearClipboardTime).not.toHaveBeenCalled(); + expect(setClearClipboardTimeMock).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/browser/src/commands/generate-password-to-clipboard-command.ts b/apps/browser/src/clipboard/generate-password-to-clipboard-command.ts similarity index 84% rename from apps/browser/src/commands/generate-password-to-clipboard-command.ts rename to apps/browser/src/clipboard/generate-password-to-clipboard-command.ts index 3f14412201a4..ca92d2c686f1 100644 --- a/apps/browser/src/commands/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/clipboard/generate-password-to-clipboard-command.ts @@ -2,6 +2,7 @@ import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwo import { StateService } from "../services/abstractions/state.service"; +import { setClearClipboardTime } from "./clipboard-state"; import { copyToClipboard } from "./copy-to-clipboard-command"; export class GeneratePasswordToClipboardCommand { @@ -19,7 +20,7 @@ export class GeneratePasswordToClipboardCommand { const clearClipboard = await this.stateService.getClearClipboard(); if (clearClipboard != null) { - await this.stateService.setClearClipboardTime(Date.now() + clearClipboard * 1000); + await setClearClipboardTime(this.stateService, Date.now() + clearClipboard * 1000); } } } diff --git a/apps/browser/src/clipboard/index.ts b/apps/browser/src/clipboard/index.ts new file mode 100644 index 000000000000..9a6cfde55f5f --- /dev/null +++ b/apps/browser/src/clipboard/index.ts @@ -0,0 +1,3 @@ +export * from "./clearClipboard"; +export * from "./copy-to-clipboard-command"; +export * from "./generate-password-to-clipboard-command"; diff --git a/apps/browser/src/services/abstractions/state.service.ts b/apps/browser/src/services/abstractions/state.service.ts index e0bdedbd4ebe..0f552154d6a4 100644 --- a/apps/browser/src/services/abstractions/state.service.ts +++ b/apps/browser/src/services/abstractions/state.service.ts @@ -33,6 +33,4 @@ export abstract class StateService extends BaseStateServiceAbstraction value: BrowserComponentState, options?: StorageOptions ) => Promise; - getClearClipboardTime: () => Promise; - setClearClipboardTime: (time: number) => Promise; } diff --git a/apps/browser/src/services/state.service.ts b/apps/browser/src/services/state.service.ts index f0b04188239b..78bc721031a0 100644 --- a/apps/browser/src/services/state.service.ts +++ b/apps/browser/src/services/state.service.ts @@ -129,12 +129,4 @@ export class StateService this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } - - async setClearClipboardTime(time: number): Promise { - this.setInSessionMemory("clearClipboardTime", time); - } - - async getClearClipboardTime(): Promise { - return this.getFromSessionMemory("clearClipboardTime"); - } } From 9b0331dd0eb584acdc6e935c7dc53594bfa04427 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:40:43 -0400 Subject: [PATCH 15/15] Missed some changes --- apps/browser/src/background.ts | 2 +- apps/browser/src/clipboard/clearClipboard.ts | 2 +- apps/browser/src/listeners/onCommandListener.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/background.ts b/apps/browser/src/background.ts index 45fc6f10d902..f55cc4b1962c 100644 --- a/apps/browser/src/background.ts +++ b/apps/browser/src/background.ts @@ -1,5 +1,5 @@ import MainBackground from "./background/main.background"; -import { ClearClipboard } from "./listeners/clearClipboard"; +import { ClearClipboard } from "./clipboard"; import { onCommandListener } from "./listeners/onCommandListener"; import { onInstallListener } from "./listeners/onInstallListener"; diff --git a/apps/browser/src/clipboard/clearClipboard.ts b/apps/browser/src/clipboard/clearClipboard.ts index 8a40f2d10341..00bf329f8d36 100644 --- a/apps/browser/src/clipboard/clearClipboard.ts +++ b/apps/browser/src/clipboard/clearClipboard.ts @@ -1,5 +1,5 @@ import { StateFactory } from "@bitwarden/common/factories/stateFactory"; -import { GlobalState } from "@bitwarden/common/models/domain/globalState"; +import { GlobalState } from "@bitwarden/common/models/domain/global-state"; import { stateServiceFactory } from "../background/service_factories/state-service.factory"; import { BrowserApi } from "../browser/browserApi"; diff --git a/apps/browser/src/listeners/onCommandListener.ts b/apps/browser/src/listeners/onCommandListener.ts index cd4f11814bca..c52e5cb61acf 100644 --- a/apps/browser/src/listeners/onCommandListener.ts +++ b/apps/browser/src/listeners/onCommandListener.ts @@ -13,8 +13,8 @@ import { } from "../background/service_factories/password-generation-service.factory"; import { stateServiceFactory } from "../background/service_factories/state-service.factory"; import { BrowserApi } from "../browser/browserApi"; +import { GeneratePasswordToClipboardCommand } from "../clipboard"; import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand"; -import { GeneratePasswordToClipboardCommand } from "../commands/generate-password-to-clipboard-command"; import { Account } from "../models/account"; export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => {