[PS-817] Add Generate Password Shortcut to MV3 (#3575)
* Add generate command * Add JSDoc * Minor improvements * Remove unneeded comment * Make some properties optional * Remove main.background.ts changes * One more * Lint * Make all but length optional * Address PR feedback * Move generate command code to command * Address PR feedback * Use new alarm scheme * 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 <justindbaur@users.noreply.github.com> Co-authored-by: Daniel Smith <djsmith85@users.noreply.github.com> * Missed some changes Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: Justin Baur <justindbaur@users.noreply.github.com> Co-authored-by: Daniel Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
parent
cf2d3f5382
commit
1d1986e5e3
|
@ -1,12 +1,25 @@
|
||||||
import MainBackground from "./background/main.background";
|
import MainBackground from "./background/main.background";
|
||||||
|
import { ClearClipboard } from "./clipboard";
|
||||||
import { onCommandListener } from "./listeners/onCommandListener";
|
import { onCommandListener } from "./listeners/onCommandListener";
|
||||||
import { onInstallListener } from "./listeners/onInstallListener";
|
import { onInstallListener } from "./listeners/onInstallListener";
|
||||||
|
|
||||||
|
type AlarmAction = (executionTime: Date, serviceCache: Record<string, unknown>) => void;
|
||||||
|
|
||||||
|
const AlarmActions: AlarmAction[] = [ClearClipboard.run];
|
||||||
|
|
||||||
const manifest = chrome.runtime.getManifest();
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
|
||||||
if (manifest.manifest_version === 3) {
|
if (manifest.manifest_version === 3) {
|
||||||
chrome.commands.onCommand.addListener(onCommandListener);
|
chrome.commands.onCommand.addListener(onCommandListener);
|
||||||
chrome.runtime.onInstalled.addListener(onInstallListener);
|
chrome.runtime.onInstalled.addListener(onInstallListener);
|
||||||
|
chrome.alarms.onAlarm.addListener((_alarm) => {
|
||||||
|
const executionTime = new Date();
|
||||||
|
const serviceCache = {};
|
||||||
|
|
||||||
|
for (const alarmAction of AlarmActions) {
|
||||||
|
alarmAction(executionTime, serviceCache);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
|
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
|
||||||
bitwardenMain.bootstrap().then(() => {
|
bitwardenMain.bootstrap().then(() => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { CryptoService as AbstractCryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
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 {
|
import {
|
||||||
cryptoFunctionServiceFactory,
|
cryptoFunctionServiceFactory,
|
||||||
|
@ -32,7 +33,7 @@ export function cryptoServiceFactory(
|
||||||
"cryptoService",
|
"cryptoService",
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new CryptoService(
|
new BrowserCryptoService(
|
||||||
await cryptoFunctionServiceFactory(cache, opts),
|
await cryptoFunctionServiceFactory(cache, opts),
|
||||||
await encryptServiceFactory(cache, opts),
|
await encryptServiceFactory(cache, opts),
|
||||||
await platformUtilsServiceFactory(cache, opts),
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { TabMessage } from "../types/tab-messages";
|
||||||
|
|
||||||
export class BrowserApi {
|
export class BrowserApi {
|
||||||
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
||||||
static isSafariApi: boolean =
|
static isSafariApi: boolean =
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
|
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
|
||||||
return (await browser.windows.getAll()).filter((win) => win.incognito);
|
return (await browser.windows.getAll()).filter((win) => win.incognito);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
let stateService: MockProxy<StateService>;
|
||||||
|
let serviceCache: Record<string, unknown>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stateService = mock<StateService>();
|
||||||
|
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();
|
||||||
|
|
||||||
|
getClearClipboardTimeMock.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);
|
||||||
|
|
||||||
|
setClearClipboardTimeMock.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);
|
||||||
|
|
||||||
|
getClearClipboardTimeMock.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await ClearClipboard.run(executionTime, serviceCache);
|
||||||
|
|
||||||
|
expect(jest.spyOn(BrowserApi, "getActiveTabs")).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
||||||
|
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
|
||||||
|
|
||||||
|
import { stateServiceFactory } from "../background/service_factories/state-service.factory";
|
||||||
|
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<string, unknown>) {
|
||||||
|
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 getClearClipboardTime(stateService);
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { StateService } from "../services/abstractions/state.service";
|
||||||
|
|
||||||
|
const clearClipboardStorageKey = "clearClipboardTime";
|
||||||
|
export const getClearClipboardTime = async (stateService: StateService) => {
|
||||||
|
return await stateService.getFromSessionMemory<number>(clearClipboardStorageKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setClearClipboardTime = async (stateService: StateService, time: number) => {
|
||||||
|
await stateService.setInSessionMemory(clearClipboardStorageKey, time);
|
||||||
|
};
|
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,76 @@
|
||||||
|
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<PasswordGenerationService>;
|
||||||
|
let stateService: MockProxy<StateService>;
|
||||||
|
|
||||||
|
let sut: GeneratePasswordToClipboardCommand;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
passwordGenerationService = mock<PasswordGenerationService>();
|
||||||
|
stateService = mock<StateService>();
|
||||||
|
|
||||||
|
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(setClearClipboardTimeMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(setClearClipboardTimeMock).toHaveBeenCalledWith(stateService, expect.any(Number));
|
||||||
|
});
|
||||||
|
|
||||||
|
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(setClearClipboardTimeMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
|
|
||||||
|
import { StateService } from "../services/abstractions/state.service";
|
||||||
|
|
||||||
|
import { setClearClipboardTime } from "./clipboard-state";
|
||||||
|
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) {
|
||||||
|
await setClearClipboardTime(this.stateService, Date.now() + clearClipboard * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./clearClipboard";
|
||||||
|
export * from "./copy-to-clipboard-command";
|
||||||
|
export * from "./generate-password-to-clipboard-command";
|
|
@ -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":
|
||||||
|
await copyText(msg.text);
|
||||||
|
break;
|
||||||
|
case "clearClipboard":
|
||||||
|
await copyText("\u0000");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(onMessageListener);
|
|
@ -7,7 +7,13 @@ import { authServiceFactory } from "../background/service_factories/auth-service
|
||||||
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
||||||
import { CachedServices } from "../background/service_factories/factory-options";
|
import { CachedServices } from "../background/service_factories/factory-options";
|
||||||
import { logServiceFactory } from "../background/service_factories/log-service.factory";
|
import { logServiceFactory } from "../background/service_factories/log-service.factory";
|
||||||
|
import {
|
||||||
|
passwordGenerationServiceFactory,
|
||||||
|
PasswordGenerationServiceInitOptions,
|
||||||
|
} from "../background/service_factories/password-generation-service.factory";
|
||||||
|
import { stateServiceFactory } from "../background/service_factories/state-service.factory";
|
||||||
import { BrowserApi } from "../browser/browserApi";
|
import { BrowserApi } from "../browser/browserApi";
|
||||||
|
import { GeneratePasswordToClipboardCommand } from "../clipboard";
|
||||||
import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand";
|
import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand";
|
||||||
import { Account } from "../models/account";
|
import { Account } from "../models/account";
|
||||||
|
|
||||||
|
@ -16,6 +22,9 @@ export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) =
|
||||||
case "autofill_login":
|
case "autofill_login":
|
||||||
await doAutoFillLogin(tab);
|
await doAutoFillLogin(tab);
|
||||||
break;
|
break;
|
||||||
|
case "generate_password":
|
||||||
|
await doGeneratePasswordToClipboard(tab);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,3 +78,37 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise<void> => {
|
||||||
const command = new AutoFillActiveTabCommand(autofillService);
|
const command = new AutoFillActiveTabCommand(autofillService);
|
||||||
await command.doAutoFillActiveTabCommand(tab);
|
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);
|
||||||
|
};
|
||||||
|
|
|
@ -38,6 +38,12 @@
|
||||||
"css": ["content/autofill.css"],
|
"css": ["content/autofill.css"],
|
||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||||
"run_at": "document_end"
|
"run_at": "document_end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"all_frames": true,
|
||||||
|
"js": ["content/misc-utils.js"],
|
||||||
|
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||||
|
"run_at": "document_end"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
|
@ -59,7 +65,8 @@
|
||||||
"unlimitedStorage",
|
"unlimitedStorage",
|
||||||
"clipboardRead",
|
"clipboardRead",
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"idle"
|
"idle",
|
||||||
|
"alarms"
|
||||||
],
|
],
|
||||||
"optional_permissions": ["nativeMessaging"],
|
"optional_permissions": ["nativeMessaging"],
|
||||||
"host_permissions": ["http://*/*", "https://*/*"],
|
"host_permissions": ["http://*/*", "https://*/*"],
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -218,6 +218,8 @@ if (manifestVersion == 2) {
|
||||||
return chunk.name === "background";
|
return chunk.name === "background";
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
config.entry["content/misc-utils"] = "./src/content/misc-utils.ts";
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import * as zxcvbn from "zxcvbn";
|
import * as zxcvbn from "zxcvbn";
|
||||||
|
|
||||||
import { GeneratedPasswordHistory } from "../models/domain/generated-password-history";
|
import { GeneratedPasswordHistory } from "../models/domain/generated-password-history";
|
||||||
|
import { PasswordGeneratorOptions } from "../models/domain/password-generator-options";
|
||||||
import { PasswordGeneratorPolicyOptions } from "../models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "../models/domain/password-generator-policy-options";
|
||||||
|
|
||||||
export abstract class PasswordGenerationService {
|
export abstract class PasswordGenerationService {
|
||||||
generatePassword: (options: any) => Promise<string>;
|
generatePassword: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||||
generatePassphrase: (options: any) => Promise<string>;
|
generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>;
|
||||||
getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>;
|
getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
enforcePasswordGeneratorPoliciesOnOptions: (
|
||||||
options: any
|
options: PasswordGeneratorOptions
|
||||||
) => Promise<[any, PasswordGeneratorPolicyOptions]>;
|
) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>;
|
||||||
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
||||||
saveOptions: (options: any) => Promise<any>;
|
saveOptions: (options: PasswordGeneratorOptions) => Promise<void>;
|
||||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||||
addHistory: (password: string) => Promise<any>;
|
addHistory: (password: string) => Promise<void>;
|
||||||
clear: (userId?: string) => Promise<any>;
|
clear: (userId?: string) => Promise<void>;
|
||||||
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
|
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
|
||||||
normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void;
|
normalizeOptions: (
|
||||||
|
options: PasswordGeneratorOptions,
|
||||||
|
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
};
|
|
@ -9,10 +9,11 @@ import { PolicyType } from "../enums/policyType";
|
||||||
import { EFFLongWordList } from "../misc/wordlist";
|
import { EFFLongWordList } from "../misc/wordlist";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
import { EncString } from "../models/domain/enc-string";
|
||||||
import { GeneratedPasswordHistory } from "../models/domain/generated-password-history";
|
import { GeneratedPasswordHistory } from "../models/domain/generated-password-history";
|
||||||
|
import { PasswordGeneratorOptions } from "../models/domain/password-generator-options";
|
||||||
import { PasswordGeneratorPolicyOptions } from "../models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "../models/domain/password-generator-policy-options";
|
||||||
import { Policy } from "../models/domain/policy";
|
import { Policy } from "../models/domain/policy";
|
||||||
|
|
||||||
const DefaultOptions = {
|
const DefaultOptions: PasswordGeneratorOptions = {
|
||||||
length: 14,
|
length: 14,
|
||||||
ambiguous: false,
|
ambiguous: false,
|
||||||
number: true,
|
number: true,
|
||||||
|
@ -39,7 +40,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
private stateService: StateService
|
private stateService: StateService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generatePassword(options: any): Promise<string> {
|
async generatePassword(options: PasswordGeneratorOptions): Promise<string> {
|
||||||
// overload defaults with given options
|
// overload defaults with given options
|
||||||
const o = Object.assign({}, DefaultOptions, options);
|
const o = Object.assign({}, DefaultOptions, options);
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generatePassphrase(options: any): Promise<string> {
|
async generatePassphrase(options: PasswordGeneratorOptions): Promise<string> {
|
||||||
const o = Object.assign({}, DefaultOptions, options);
|
const o = Object.assign({}, DefaultOptions, options);
|
||||||
|
|
||||||
if (o.numWords == null || o.numWords <= 2) {
|
if (o.numWords == null || o.numWords <= 2) {
|
||||||
|
@ -178,7 +179,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
return wordList.join(o.wordSeparator);
|
return wordList.join(o.wordSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
async getOptions(): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
||||||
let options = await this.stateService.getPasswordGenerationOptions();
|
let options = await this.stateService.getPasswordGenerationOptions();
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
options = Object.assign({}, DefaultOptions);
|
options = Object.assign({}, DefaultOptions);
|
||||||
|
@ -192,8 +193,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
}
|
}
|
||||||
|
|
||||||
async enforcePasswordGeneratorPoliciesOnOptions(
|
async enforcePasswordGeneratorPoliciesOnOptions(
|
||||||
options: any
|
options: PasswordGeneratorOptions
|
||||||
): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
): Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]> {
|
||||||
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
||||||
if (enforcedPolicyOptions != null) {
|
if (enforcedPolicyOptions != null) {
|
||||||
if (options.length < enforcedPolicyOptions.minLength) {
|
if (options.length < enforcedPolicyOptions.minLength) {
|
||||||
|
@ -340,7 +341,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
return enforcedOptions;
|
return enforcedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveOptions(options: any) {
|
async saveOptions(options: PasswordGeneratorOptions) {
|
||||||
await this.stateService.setPasswordGenerationOptions(options);
|
await this.stateService.setPasswordGenerationOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +364,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
: new Array<GeneratedPasswordHistory>();
|
: new Array<GeneratedPasswordHistory>();
|
||||||
}
|
}
|
||||||
|
|
||||||
async addHistory(password: string): Promise<any> {
|
async addHistory(password: string): Promise<void> {
|
||||||
// Cannot add new history if no key is available
|
// Cannot add new history if no key is available
|
||||||
const hasKey = await this.cryptoService.hasKey();
|
const hasKey = await this.cryptoService.hasKey();
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
|
@ -389,7 +390,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(userId?: string): Promise<any> {
|
async clear(userId?: string): Promise<void> {
|
||||||
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
||||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
||||||
}
|
}
|
||||||
|
@ -408,7 +409,10 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) {
|
normalizeOptions(
|
||||||
|
options: PasswordGeneratorOptions,
|
||||||
|
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
|
||||||
|
) {
|
||||||
options.minLowercase = 0;
|
options.minLowercase = 0;
|
||||||
options.minUppercase = 0;
|
options.minUppercase = 0;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue