Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PS-817] Add Generate Password Shortcut to MV3 #3575

Merged
merged 19 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/browser/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import MainBackground from "./background/main.background";
import { onAlarmListener } from "./listeners/on-alarm-listener";
import { onCommandListener } from "./listeners/onCommandListener";
import { onInstallListener } from "./listeners/onInstallListener";

Expand All @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AbstractAutofillService> {
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)
)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function cipherServiceFactory(
await apiServiceFactory(cache, opts),
await fileUploadServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
opts.cipherServiceOptions.searchServiceFactory === undefined
opts.cipherServiceOptions?.searchServiceFactory === undefined
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
? () => cache.searchService
: opts.cipherServiceOptions.searchServiceFactory,
await logServiceFactory(cache, opts),
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EventService as AbstractEventService } from "@bitwarden/common/abstractions/event.service";
import { EventService } from "@bitwarden/common/services/event.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;

export type EventServiceInitOptions = EventServiceFactoryOptions &
ApiServiceInitOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
LogServiceInitOptions &
OrganizationServiceInitOptions;

export function eventServiceFactory(
cache: { eventService?: AbstractEventService } & CachedServices,
opts: EventServiceInitOptions
): Promise<AbstractEventService> {
return factory(cache, "eventService", opts, async () => {
return new EventService(
await apiServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await organizationServiceFactory(cache, opts)
);
});
}
Original file line number Diff line number Diff line change
@@ -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<AbstractPasswordGenerationService> {
return factory(
cache,
"passwordGenerationService",
opts,
async () =>
new PasswordGenerationService(
await cryptoServiceFactory(cache, opts),
await policyServiceFactory(cache, opts),
await stateServiceFactory(cache, opts)
)
);
}
Original file line number Diff line number Diff line change
@@ -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<AbstractTotpService> {
return factory(
cache,
"totpService",
opts,
async () =>
new TotpService(
await cryptoFunctionServiceFactory(cache, opts),
await logServiceFactory(cache, opts)
)
);
}
10 changes: 10 additions & 0 deletions apps/browser/src/browser/browserApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TabMessage } from "../types/tab-messages";

export class BrowserApi {
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
static isSafariApi: boolean =
Expand Down Expand Up @@ -80,6 +82,14 @@ export class BrowserApi {
});
}

static sendTabsMessage<T = never>(
tabId: number,
message: TabMessage,
responseCallback?: (response: T) => void
) {
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, responseCallback);
}

justindbaur marked this conversation as resolved.
Show resolved Hide resolved
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
return (await browser.windows.getAll()).filter((win) => win.incognito);
}
Expand Down
17 changes: 17 additions & 0 deletions apps/browser/src/commands/copy-to-clipboard-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BrowserApi } from "../browser/browserApi";

/**
* 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.");
}

BrowserApi.sendTabsMessage(tab.id, {
command: "copyText",
text: text,
});
};
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
}
23 changes: 23 additions & 0 deletions apps/browser/src/content/misc-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TabMessage } from "../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":
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
await copyText(msg.text);
break;
case "clearClipboard":
await copyText("\u0000");
break;
default:
}
}

chrome.runtime.onMessage.addListener(onMessageListener);
15 changes: 15 additions & 0 deletions apps/browser/src/listeners/on-alarm-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BrowserApi } from "../browser/browserApi";

export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => {
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
};
43 changes: 43 additions & 0 deletions apps/browser/src/listeners/onCommandListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +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 {
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";
Expand All @@ -28,6 +34,9 @@ export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) =
case "autofill_login":
await doAutoFillLogin(tab);
break;
case "generate_password":
await doGeneratePasswordToClipboard(tab);
break;
}
};

Expand Down Expand Up @@ -137,3 +146,37 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise<void> => {
const command = new AutoFillActiveTabCommand(autofillService);
await command.doAutoFillActiveTabCommand(tab);
};

const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise<void> => {
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 command = new GeneratePasswordToClipboardCommand(
await passwordGenerationServiceFactory(cache, options),
await stateServiceFactory(cache, options)
);
command.generatePasswordToClipboard(tab);
};
9 changes: 8 additions & 1 deletion apps/browser/src/manifest.v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
"css": ["content/autofill.css"],
"matches": ["http://*/*", "https://*/*", "file:///*"],
"run_at": "document_end"
},
{
"all_frames": true,
"js": ["content/misc-utils.js"],
"matches": ["http://*/*", "https://*/*", "file:///*"],
"run_at": "document_end"
}
],
"background": {
Expand All @@ -59,7 +65,8 @@
"unlimitedStorage",
"clipboardRead",
"clipboardWrite",
"idle"
"idle",
"alarms"
],
"optional_permissions": ["nativeMessaging"],
"host_permissions": ["http://*/*", "https://*/*"],
Expand Down
9 changes: 9 additions & 0 deletions apps/browser/src/types/tab-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type TabMessage = CopyTextTabMessage | TabMessageBase<"clearClipboard">;

export type TabMessageBase<T extends string> = {
command: T;
};

export type CopyTextTabMessage = TabMessageBase<"copyText"> & {
text: string;
};
2 changes: 2 additions & 0 deletions apps/browser/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ if (manifestVersion == 2) {
return chunk.name === "background";
},
};
} else {
config.entry["content/misc-utils"] = "./src/content/misc-utils.ts";
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
}

module.exports = config;
Loading