mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -2000,6 +2000,12 @@
|
||||
"nativeMessagingWrongUserTitle": {
|
||||
"message": "Account missmatch"
|
||||
},
|
||||
"nativeMessagingWrongUserKeyDesc": {
|
||||
"message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again."
|
||||
},
|
||||
"nativeMessagingWrongUserKeyTitle": {
|
||||
"message": "Biometric key missmatch"
|
||||
},
|
||||
"biometricsNotEnabledTitle": {
|
||||
"message": "Biometrics not set up"
|
||||
},
|
||||
@@ -4091,6 +4097,12 @@
|
||||
"itemLocation": {
|
||||
"message": "Item Location"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "File Sends"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
},
|
||||
"bitwardenNewLook": {
|
||||
"message": "Bitwarden has a new look!"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperData } from "./extension-anon-layout-wrapper.component";
|
||||
|
||||
export class ExtensionAnonLayoutWrapperDataService
|
||||
extends DefaultAnonLayoutWrapperDataService
|
||||
implements AnonLayoutWrapperDataService
|
||||
{
|
||||
protected override anonLayoutWrapperDataSubject = new Subject<ExtensionAnonLayoutWrapperData>();
|
||||
|
||||
override setAnonLayoutWrapperData(data: ExtensionAnonLayoutWrapperData): void {
|
||||
this.anonLayoutWrapperDataSubject.next(data);
|
||||
}
|
||||
|
||||
override anonLayoutWrapperData$(): Observable<ExtensionAnonLayoutWrapperData> {
|
||||
return this.anonLayoutWrapperDataSubject.asObservable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[background]="'alt'"
|
||||
[showBackButton]="showBackButton"
|
||||
[pageTitle]="''"
|
||||
>
|
||||
<bit-icon *ngIf="showLogo" [icon]="logo"></bit-icon>
|
||||
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account *ngIf="showAcctSwitcher"></app-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<auth-anon-layout
|
||||
[title]="pageTitle"
|
||||
[subtitle]="pageSubtitle"
|
||||
[icon]="pageIcon"
|
||||
[showReadonlyHostname]="showReadonlyHostname"
|
||||
[hideLogo]="true"
|
||||
[decreaseTopPadding]="true"
|
||||
>
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
<router-outlet slot="environment-selector" name="environment-selector"></router-outlet>
|
||||
</auth-anon-layout>
|
||||
</popup-page>
|
||||
@@ -0,0 +1,190 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
||||
import { Subject, filter, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import {
|
||||
AnonLayoutComponent,
|
||||
AnonLayoutWrapperData,
|
||||
AnonLayoutWrapperDataService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { Icon, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import { CurrentAccountComponent } from "../account-switching/current-account.component";
|
||||
|
||||
import {
|
||||
ExtensionBitwardenLogoPrimary,
|
||||
ExtensionBitwardenLogoWhite,
|
||||
} from "./extension-bitwarden-logo.icon";
|
||||
|
||||
export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
|
||||
showAcctSwitcher?: boolean;
|
||||
showBackButton?: boolean;
|
||||
showLogo?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "extension-anon-layout-wrapper.component.html",
|
||||
imports: [
|
||||
AnonLayoutComponent,
|
||||
CommonModule,
|
||||
CurrentAccountComponent,
|
||||
IconModule,
|
||||
PopOutComponent,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected showAcctSwitcher: boolean;
|
||||
protected showBackButton: boolean;
|
||||
protected showLogo: boolean = true;
|
||||
|
||||
protected pageTitle: string;
|
||||
protected pageSubtitle: string;
|
||||
protected pageIcon: Icon;
|
||||
protected showReadonlyHostname: boolean;
|
||||
protected maxWidth: "md" | "3xl";
|
||||
|
||||
protected theme: string;
|
||||
protected logo: Icon;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
private themeStateService: ThemeStateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Set the initial page data on load
|
||||
this.setAnonLayoutWrapperDataFromRouteData(this.route.snapshot.firstChild?.data);
|
||||
|
||||
// Listen for page changes and update the page data appropriately
|
||||
this.listenForPageDataChanges();
|
||||
this.listenForServiceDataChanges();
|
||||
|
||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||
|
||||
if (this.theme === "dark") {
|
||||
this.logo = ExtensionBitwardenLogoWhite;
|
||||
} else {
|
||||
this.logo = ExtensionBitwardenLogoPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
private listenForPageDataChanges() {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
// reset page data on page changes
|
||||
tap(() => this.resetPageData()),
|
||||
switchMap(() => this.route.firstChild?.data || null),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((firstChildRouteData: Data | null) => {
|
||||
this.setAnonLayoutWrapperDataFromRouteData(firstChildRouteData);
|
||||
});
|
||||
}
|
||||
|
||||
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData: Data | null) {
|
||||
if (!firstChildRouteData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstChildRouteData["pageTitle"] !== undefined) {
|
||||
this.pageTitle = this.i18nService.t(firstChildRouteData["pageTitle"]);
|
||||
}
|
||||
|
||||
if (firstChildRouteData["pageSubtitle"] !== undefined) {
|
||||
this.pageSubtitle = this.i18nService.t(firstChildRouteData["pageSubtitle"]);
|
||||
}
|
||||
|
||||
if (firstChildRouteData["pageIcon"] !== undefined) {
|
||||
this.pageIcon = firstChildRouteData["pageIcon"];
|
||||
}
|
||||
|
||||
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
|
||||
this.maxWidth = firstChildRouteData["maxWidth"];
|
||||
|
||||
if (firstChildRouteData["showAcctSwitcher"] !== undefined) {
|
||||
this.showAcctSwitcher = Boolean(firstChildRouteData["showAcctSwitcher"]);
|
||||
}
|
||||
|
||||
if (firstChildRouteData["showBackButton"] !== undefined) {
|
||||
this.showBackButton = Boolean(firstChildRouteData["showBackButton"]);
|
||||
}
|
||||
|
||||
if (firstChildRouteData["showLogo"] !== undefined) {
|
||||
this.showLogo = Boolean(firstChildRouteData["showLogo"]);
|
||||
}
|
||||
}
|
||||
|
||||
private listenForServiceDataChanges() {
|
||||
this.extensionAnonLayoutWrapperDataService
|
||||
.anonLayoutWrapperData$()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((data: ExtensionAnonLayoutWrapperData) => {
|
||||
this.setAnonLayoutWrapperData(data);
|
||||
});
|
||||
}
|
||||
|
||||
private setAnonLayoutWrapperData(data: ExtensionAnonLayoutWrapperData) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.pageTitle) {
|
||||
this.pageTitle = this.i18nService.t(data.pageTitle);
|
||||
}
|
||||
|
||||
if (data.pageSubtitle) {
|
||||
this.pageSubtitle = this.i18nService.t(data.pageSubtitle);
|
||||
}
|
||||
|
||||
if (data.pageIcon) {
|
||||
this.pageIcon = data.pageIcon;
|
||||
}
|
||||
|
||||
if (data.showReadonlyHostname != null) {
|
||||
this.showReadonlyHostname = data.showReadonlyHostname;
|
||||
}
|
||||
|
||||
if (data.showAcctSwitcher != null) {
|
||||
this.showAcctSwitcher = data.showAcctSwitcher;
|
||||
}
|
||||
|
||||
if (data.showBackButton != null) {
|
||||
this.showBackButton = data.showBackButton;
|
||||
}
|
||||
|
||||
if (data.showLogo != null) {
|
||||
this.showLogo = data.showLogo;
|
||||
}
|
||||
}
|
||||
|
||||
private resetPageData() {
|
||||
this.pageTitle = null;
|
||||
this.pageSubtitle = null;
|
||||
this.pageIcon = null;
|
||||
this.showReadonlyHostname = null;
|
||||
this.showAcctSwitcher = null;
|
||||
this.showBackButton = null;
|
||||
this.showLogo = null;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { importProvidersFrom, Component } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import {
|
||||
Meta,
|
||||
StoryObj,
|
||||
applicationConfig,
|
||||
componentWrapperDecorator,
|
||||
moduleMetadata,
|
||||
} from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AnonLayoutWrapperDataService, LockIcon } from "@bitwarden/auth/angular";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Environment,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
|
||||
import {
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
ExtensionAnonLayoutWrapperData,
|
||||
} from "./extension-anon-layout-wrapper.component";
|
||||
|
||||
export default {
|
||||
title: "Auth/Extension Anon Layout Wrapper",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
} as Meta;
|
||||
|
||||
const decorators = (options: {
|
||||
components: any[];
|
||||
routes: Routes;
|
||||
applicationVersion?: string;
|
||||
clientType?: ClientType;
|
||||
hostName?: string;
|
||||
themeType?: ThemeType;
|
||||
}) => {
|
||||
return [
|
||||
componentWrapperDecorator(
|
||||
/**
|
||||
* Applying a CSS transform makes a `position: fixed` element act like it is `position: relative`
|
||||
* https://github.com/storybookjs/storybook/issues/8011#issue-490251969
|
||||
*/
|
||||
(story) => {
|
||||
return /* HTML */ `<div class="tw-scale-100 ">${story}</div>`;
|
||||
},
|
||||
({ globals }) => {
|
||||
/**
|
||||
* avoid a bug with the way that we render the same component twice in the same iframe and how
|
||||
* that interacts with the router-outlet
|
||||
*/
|
||||
const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"];
|
||||
return { theme: themeOverride };
|
||||
},
|
||||
),
|
||||
moduleMetadata({
|
||||
declarations: options.components,
|
||||
imports: [RouterModule, ButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: AnonLayoutWrapperDataService,
|
||||
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
id: "test-user-id" as UserId,
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {
|
||||
activeAccountStatus$: of(AuthenticationStatus.Unlocked),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AvatarService,
|
||||
useValue: {
|
||||
avatarColor$: of("#ab134a"),
|
||||
} as Partial<AvatarService>,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
getFeatureFlag: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
environment$: of({
|
||||
getHostname: () => options.hostName || "storybook.bitwarden.com",
|
||||
} as Partial<Environment>),
|
||||
} as Partial<EnvironmentService>,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
getApplicationVersion: () =>
|
||||
Promise.resolve(options.applicationVersion || "FAKE_APP_VERSION"),
|
||||
getClientType: () => options.clientType || ClientType.Web,
|
||||
} as Partial<PlatformUtilsService>,
|
||||
},
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: {
|
||||
selectedTheme$: of(options.themeType || ThemeType.Light),
|
||||
} as Partial<ThemeStateService>,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
setAStrongPassword: "Set a strong password",
|
||||
finishCreatingYourAccountBySettingAPassword:
|
||||
"Finish creating your account by setting a password",
|
||||
enterpriseSingleSignOn: "Enterprise single sign-on",
|
||||
checkYourEmail: "Check your email",
|
||||
loading: "Loading",
|
||||
popOutNewWindow: "Pop out to a new window",
|
||||
switchAccounts: "Switch accounts",
|
||||
back: "Back",
|
||||
activeAccount: "Active account",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(RouterModule.forRoot(options.routes))],
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
type Story = StoryObj<ExtensionAnonLayoutWrapperComponent>;
|
||||
|
||||
// Default Example
|
||||
|
||||
@Component({
|
||||
selector: "bit-default-primary-outlet-example-component",
|
||||
template: "<p>Primary Outlet Example: <br> your primary component goes here</p>",
|
||||
})
|
||||
class DefaultPrimaryOutletExampleComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "bit-default-secondary-outlet-example-component",
|
||||
template: "<p>Secondary Outlet Example: <br> your secondary component goes here</p>",
|
||||
})
|
||||
class DefaultSecondaryOutletExampleComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "bit-default-env-selector-outlet-example-component",
|
||||
template: "<p>Env Selector Outlet Example: <br> your env selector component goes here</p>",
|
||||
})
|
||||
class DefaultEnvSelectorOutletExampleComponent {}
|
||||
|
||||
export const DefaultContentExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: "<router-outlet></router-outlet>",
|
||||
}),
|
||||
decorators: decorators({
|
||||
components: [
|
||||
DefaultPrimaryOutletExampleComponent,
|
||||
DefaultSecondaryOutletExampleComponent,
|
||||
DefaultEnvSelectorOutletExampleComponent,
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
path: "**",
|
||||
redirectTo: "default-example",
|
||||
pathMatch: "full",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "default-example",
|
||||
data: {},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: DefaultPrimaryOutletExampleComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: DefaultSecondaryOutletExampleComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: DefaultEnvSelectorOutletExampleComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
// Dynamic Content Example
|
||||
const initialData: ExtensionAnonLayoutWrapperData = {
|
||||
pageTitle: "setAStrongPassword",
|
||||
pageSubtitle: "finishCreatingYourAccountBySettingAPassword",
|
||||
pageIcon: LockIcon,
|
||||
showAcctSwitcher: true,
|
||||
showBackButton: true,
|
||||
showLogo: true,
|
||||
};
|
||||
|
||||
const changedData: ExtensionAnonLayoutWrapperData = {
|
||||
pageTitle: "enterpriseSingleSignOn",
|
||||
pageSubtitle: "checkYourEmail",
|
||||
pageIcon: RegistrationCheckEmailIcon,
|
||||
showAcctSwitcher: false,
|
||||
showBackButton: false,
|
||||
showLogo: false,
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "bit-dynamic-content-example-component",
|
||||
template: `
|
||||
<button type="button" bitButton buttonType="primary" (click)="toggleData()">Toggle Data</button>
|
||||
`,
|
||||
})
|
||||
export class DynamicContentExampleComponent {
|
||||
initialData = true;
|
||||
|
||||
constructor(private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService) {}
|
||||
|
||||
toggleData() {
|
||||
if (this.initialData) {
|
||||
this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData(changedData);
|
||||
} else {
|
||||
this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData(initialData);
|
||||
}
|
||||
|
||||
this.initialData = !this.initialData;
|
||||
}
|
||||
}
|
||||
|
||||
export const DynamicContentExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: "<router-outlet></router-outlet>",
|
||||
}),
|
||||
decorators: decorators({
|
||||
components: [DynamicContentExampleComponent],
|
||||
routes: [
|
||||
{
|
||||
path: "**",
|
||||
redirectTo: "dynamic-content-example",
|
||||
pathMatch: "full",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "dynamic-content-example",
|
||||
data: initialData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: DynamicContentExampleComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -95,6 +95,10 @@ export type OverlayAddNewItemMessage = {
|
||||
identity?: NewIdentityCipherData;
|
||||
};
|
||||
|
||||
export type CurrentAddNewItemData = OverlayAddNewItemMessage & {
|
||||
sender: chrome.runtime.MessageSender;
|
||||
};
|
||||
|
||||
export type CloseInlineMenuMessage = {
|
||||
forceCloseInlineMenu?: boolean;
|
||||
overlayElement?: string;
|
||||
@@ -161,7 +165,7 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
||||
triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void;
|
||||
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
updateIsFieldCurrentlyFocused: ({ message }: BackgroundMessageParam) => void;
|
||||
updateIsFieldCurrentlyFocused: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
checkIsFieldCurrentlyFocused: () => boolean;
|
||||
updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void;
|
||||
checkIsFieldCurrentlyFilling: () => boolean;
|
||||
|
||||
@@ -176,8 +176,12 @@ describe("OverlayBackground", () => {
|
||||
parentFrameId: getFrameCounter,
|
||||
});
|
||||
});
|
||||
tabsSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage");
|
||||
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData");
|
||||
tabsSendMessageSpy = jest
|
||||
.spyOn(BrowserApi, "tabSendMessage")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
tabSendMessageDataSpy = jest
|
||||
.spyOn(BrowserApi, "tabSendMessageData")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
|
||||
getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
|
||||
getTabSpy = jest.spyOn(BrowserApi, "getTab");
|
||||
@@ -526,10 +530,13 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
|
||||
it("skips updating the position of either inline menu element if a field is not currently focused", async () => {
|
||||
sendMockExtensionMessage({
|
||||
command: "updateIsFieldCurrentlyFocused",
|
||||
isFieldCurrentlyFocused: false,
|
||||
});
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "updateIsFieldCurrentlyFocused",
|
||||
isFieldCurrentlyFocused: false,
|
||||
},
|
||||
mock<chrome.runtime.MessageSender>({ frameId: 20 }),
|
||||
);
|
||||
|
||||
sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender);
|
||||
await flushUpdateInlineMenuPromises();
|
||||
@@ -835,7 +842,7 @@ describe("OverlayBackground", () => {
|
||||
|
||||
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
|
||||
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1]);
|
||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||
|
||||
@@ -854,7 +861,7 @@ describe("OverlayBackground", () => {
|
||||
image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png",
|
||||
imageEnabled: true,
|
||||
},
|
||||
id: "inline-menu-cipher-1",
|
||||
id: "inline-menu-cipher-0",
|
||||
login: {
|
||||
username: "username-1",
|
||||
},
|
||||
@@ -1116,10 +1123,12 @@ describe("OverlayBackground", () => {
|
||||
let openAddEditVaultItemPopoutSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
|
||||
openAddEditVaultItemPopoutSpy = jest
|
||||
.spyOn(overlayBackground as any, "openAddEditVaultItemPopout")
|
||||
.mockImplementation();
|
||||
overlayBackground["currentAddNewItemData"] = { sender, addNewCipherType: CipherType.Login };
|
||||
});
|
||||
|
||||
it("will not open the add edit popout window if the message does not have a login cipher provided", () => {
|
||||
@@ -1129,6 +1138,28 @@ describe("OverlayBackground", () => {
|
||||
expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resets the currentAddNewItemData to null when a cipher view is not successfully created", async () => {
|
||||
jest.spyOn(overlayBackground as any, "buildLoginCipherView").mockReturnValue(null);
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "autofillOverlayAddNewVaultItem",
|
||||
addNewCipherType: CipherType.Login,
|
||||
login: {
|
||||
uri: "https://tacos.com",
|
||||
hostname: "",
|
||||
username: "username",
|
||||
password: "password",
|
||||
},
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayBackground["currentAddNewItemData"]).toBeNull();
|
||||
});
|
||||
|
||||
it("will open the add edit popout window after creating a new cipher", async () => {
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
@@ -1143,6 +1174,7 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
@@ -1151,6 +1183,8 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
|
||||
it("creates a new card cipher", async () => {
|
||||
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card;
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "autofillOverlayAddNewVaultItem",
|
||||
@@ -1166,6 +1200,7 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
@@ -1174,6 +1209,10 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
|
||||
describe("creating a new identity cipher", () => {
|
||||
beforeEach(() => {
|
||||
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity;
|
||||
});
|
||||
|
||||
it("populates an identity cipher view and creates it", async () => {
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
@@ -1200,6 +1239,7 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
@@ -1220,6 +1260,7 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
@@ -1238,6 +1279,7 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
@@ -1256,11 +1298,173 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
sender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("pulling cipher data from multiple frames of a tab", () => {
|
||||
let subFrameSender: MockProxy<chrome.runtime.MessageSender>;
|
||||
const command = "autofillOverlayAddNewVaultItem";
|
||||
|
||||
beforeEach(() => {
|
||||
subFrameSender = mock<chrome.runtime.MessageSender>({ tab: sender.tab, frameId: 2 });
|
||||
});
|
||||
|
||||
it("combines the login cipher data from all frames", async () => {
|
||||
const buildLoginCipherViewSpy = jest.spyOn(
|
||||
overlayBackground as any,
|
||||
"buildLoginCipherView",
|
||||
);
|
||||
const addNewCipherType = CipherType.Login;
|
||||
const loginCipherData = {
|
||||
uri: "https://tacos.com",
|
||||
hostname: "",
|
||||
username: "username",
|
||||
password: "",
|
||||
};
|
||||
const subFrameLoginCipherData = {
|
||||
uri: "https://tacos.com",
|
||||
hostname: "tacos.com",
|
||||
username: "",
|
||||
password: "password",
|
||||
};
|
||||
|
||||
sendMockExtensionMessage({ command, addNewCipherType, login: loginCipherData }, sender);
|
||||
sendMockExtensionMessage(
|
||||
{ command, addNewCipherType, login: subFrameLoginCipherData },
|
||||
subFrameSender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(buildLoginCipherViewSpy).toHaveBeenCalledWith({
|
||||
uri: "https://tacos.com",
|
||||
hostname: "tacos.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
});
|
||||
});
|
||||
|
||||
it("combines the card cipher data from all frames", async () => {
|
||||
const buildCardCipherViewSpy = jest.spyOn(
|
||||
overlayBackground as any,
|
||||
"buildCardCipherView",
|
||||
);
|
||||
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card;
|
||||
const addNewCipherType = CipherType.Card;
|
||||
const cardCipherData = {
|
||||
cardholderName: "cardholderName",
|
||||
number: "",
|
||||
expirationMonth: "",
|
||||
expirationYear: "",
|
||||
expirationDate: "12/25",
|
||||
cvv: "123",
|
||||
};
|
||||
const subFrameCardCipherData = {
|
||||
cardholderName: "",
|
||||
number: "4242424242424242",
|
||||
expirationMonth: "12",
|
||||
expirationYear: "2025",
|
||||
expirationDate: "",
|
||||
cvv: "",
|
||||
};
|
||||
|
||||
sendMockExtensionMessage({ command, addNewCipherType, card: cardCipherData }, sender);
|
||||
sendMockExtensionMessage(
|
||||
{ command, addNewCipherType, card: subFrameCardCipherData },
|
||||
subFrameSender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(buildCardCipherViewSpy).toHaveBeenCalledWith({
|
||||
cardholderName: "cardholderName",
|
||||
number: "4242424242424242",
|
||||
expirationMonth: "12",
|
||||
expirationYear: "2025",
|
||||
expirationDate: "12/25",
|
||||
cvv: "123",
|
||||
});
|
||||
});
|
||||
|
||||
it("combines the identity cipher data from all frames", async () => {
|
||||
const buildIdentityCipherViewSpy = jest.spyOn(
|
||||
overlayBackground as any,
|
||||
"buildIdentityCipherView",
|
||||
);
|
||||
overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity;
|
||||
const addNewCipherType = CipherType.Identity;
|
||||
const identityCipherData = {
|
||||
title: "title",
|
||||
firstName: "firstName",
|
||||
middleName: "middleName",
|
||||
lastName: "",
|
||||
fullName: "",
|
||||
address1: "address1",
|
||||
address2: "address2",
|
||||
address3: "address3",
|
||||
city: "city",
|
||||
state: "state",
|
||||
postalCode: "postalCode",
|
||||
country: "country",
|
||||
company: "company",
|
||||
phone: "phone",
|
||||
email: "email",
|
||||
username: "username",
|
||||
};
|
||||
const subFrameIdentityCipherData = {
|
||||
title: "",
|
||||
firstName: "",
|
||||
middleName: "",
|
||||
lastName: "lastName",
|
||||
fullName: "fullName",
|
||||
address1: "",
|
||||
address2: "",
|
||||
address3: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "",
|
||||
company: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
username: "",
|
||||
};
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{ command, addNewCipherType, identity: identityCipherData },
|
||||
sender,
|
||||
);
|
||||
sendMockExtensionMessage(
|
||||
{ command, addNewCipherType, identity: subFrameIdentityCipherData },
|
||||
subFrameSender,
|
||||
);
|
||||
jest.advanceTimersByTime(100);
|
||||
await flushPromises();
|
||||
|
||||
expect(buildIdentityCipherViewSpy).toHaveBeenCalledWith({
|
||||
title: "title",
|
||||
firstName: "firstName",
|
||||
middleName: "middleName",
|
||||
lastName: "lastName",
|
||||
fullName: "fullName",
|
||||
address1: "address1",
|
||||
address2: "address2",
|
||||
address3: "address3",
|
||||
city: "city",
|
||||
state: "state",
|
||||
postalCode: "postalCode",
|
||||
country: "country",
|
||||
company: "company",
|
||||
phone: "phone",
|
||||
email: "email",
|
||||
username: "username",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkIsInlineMenuCiphersPopulated message handler", () => {
|
||||
@@ -1360,6 +1564,70 @@ describe("OverlayBackground", () => {
|
||||
showInlineMenuAccountCreation: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("triggers an update of the inline menu ciphers when the new focused field's cipher type does not equal the previous focused field's cipher type", async () => {
|
||||
const updateOverlayCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers");
|
||||
const tab = createChromeTabMock({ id: 2 });
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
|
||||
const focusedFieldData = createFocusedFieldDataMock({
|
||||
tabId: tab.id,
|
||||
frameId: sender.frameId,
|
||||
filledByCipherType: CipherType.Login,
|
||||
});
|
||||
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
|
||||
await flushPromises();
|
||||
|
||||
const newFocusedFieldData = createFocusedFieldDataMock({
|
||||
tabId: tab.id,
|
||||
frameId: sender.frameId,
|
||||
filledByCipherType: CipherType.Card,
|
||||
});
|
||||
sendMockExtensionMessage(
|
||||
{ command: "updateFocusedFieldData", focusedFieldData: newFocusedFieldData },
|
||||
sender,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(updateOverlayCiphersSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateIsFieldCurrentlyFocused message handler", () => {
|
||||
it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => {
|
||||
const focusedFieldData = createFocusedFieldDataMock();
|
||||
sendMockExtensionMessage(
|
||||
{ command: "updateFocusedFieldData", focusedFieldData },
|
||||
mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 10 }),
|
||||
);
|
||||
overlayBackground["isFieldCurrentlyFocused"] = true;
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{ command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false },
|
||||
mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 20 }),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateIsFieldCurrentlyFocused message handler", () => {
|
||||
it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => {
|
||||
const focusedFieldData = createFocusedFieldDataMock();
|
||||
sendMockExtensionMessage(
|
||||
{ command: "updateFocusedFieldData", focusedFieldData },
|
||||
mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 10 }),
|
||||
);
|
||||
overlayBackground["isFieldCurrentlyFocused"] = true;
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{ command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false },
|
||||
mock<chrome.runtime.MessageSender>({ tab: { id: 1 }, frameId: 20 }),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkIsFieldCurrentlyFocused message handler", () => {
|
||||
@@ -1819,7 +2087,6 @@ describe("OverlayBackground", () => {
|
||||
overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([
|
||||
[focusedFieldData.frameId, null],
|
||||
]);
|
||||
tabsSendMessageSpy.mockImplementation();
|
||||
jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent");
|
||||
|
||||
sendMockExtensionMessage(
|
||||
@@ -2068,7 +2335,6 @@ describe("OverlayBackground", () => {
|
||||
describe("autofillInlineMenuButtonClicked message handler", () => {
|
||||
it("opens the unlock vault popout if the user auth status is not unlocked", async () => {
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
tabsSendMessageSpy.mockImplementation();
|
||||
|
||||
sendPortMessage(buttonMessageConnectorSpy, {
|
||||
command: "autofillInlineMenuButtonClicked",
|
||||
@@ -2269,7 +2535,6 @@ describe("OverlayBackground", () => {
|
||||
describe("unlockVault message handler", () => {
|
||||
it("opens the unlock vault popout", async () => {
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
tabsSendMessageSpy.mockImplementation();
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey });
|
||||
await flushPromises();
|
||||
@@ -2421,11 +2686,10 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
|
||||
sender.tab,
|
||||
{ command: "addNewVaultItemFromOverlay", addNewCipherType: CipherType.Login },
|
||||
{ frameId: sender.frameId },
|
||||
);
|
||||
expect(tabsSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
|
||||
command: "addNewVaultItemFromOverlay",
|
||||
addNewCipherType: CipherType.Login,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import { generateRandomChars } from "../utils";
|
||||
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
|
||||
import {
|
||||
CloseInlineMenuMessage,
|
||||
CurrentAddNewItemData,
|
||||
FocusedFieldData,
|
||||
InlineMenuButtonPortMessageHandlers,
|
||||
InlineMenuCipherData,
|
||||
@@ -83,6 +84,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private cancelUpdateInlineMenuPositionSubject = new Subject<void>();
|
||||
private repositionInlineMenuSubject = new Subject<chrome.runtime.MessageSender>();
|
||||
private rebuildSubFrameOffsetsSubject = new Subject<chrome.runtime.MessageSender>();
|
||||
private addNewVaultItemSubject = new Subject<CurrentAddNewItemData>();
|
||||
private currentAddNewItemData: CurrentAddNewItemData;
|
||||
private focusedFieldData: FocusedFieldData;
|
||||
private isFieldCurrentlyFocused: boolean = false;
|
||||
private isFieldCurrentlyFilling: boolean = false;
|
||||
@@ -97,7 +100,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
checkIsInlineMenuCiphersPopulated: ({ sender }) =>
|
||||
this.checkIsInlineMenuCiphersPopulated(sender),
|
||||
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
|
||||
updateIsFieldCurrentlyFocused: ({ message }) => this.updateIsFieldCurrentlyFocused(message),
|
||||
updateIsFieldCurrentlyFocused: ({ message, sender }) =>
|
||||
this.updateIsFieldCurrentlyFocused(message, sender),
|
||||
checkIsFieldCurrentlyFocused: () => this.checkIsFieldCurrentlyFocused(),
|
||||
updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message),
|
||||
checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(),
|
||||
@@ -186,6 +190,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
switchMap((sender) => this.rebuildSubFrameOffsets(sender)),
|
||||
)
|
||||
.subscribe();
|
||||
this.addNewVaultItemSubject
|
||||
.pipe(
|
||||
debounceTime(100),
|
||||
switchMap((addNewItemData) =>
|
||||
this.buildCipherAndOpenAddEditVaultItemPopout(addNewItemData),
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Debounce used to update inline menu position
|
||||
merge(
|
||||
@@ -230,14 +242,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
if (this.focusedFieldData) {
|
||||
void this.closeInlineMenuAfterCiphersUpdate();
|
||||
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = await BrowserApi.getTabFromCurrentWindowId();
|
||||
if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) {
|
||||
void this.closeInlineMenuAfterCiphersUpdate();
|
||||
this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
this.inlineMenuCiphers = new Map();
|
||||
@@ -318,7 +330,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private async getInlineMenuCipherData(): Promise<InlineMenuCipherData[]> {
|
||||
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||
const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers);
|
||||
let inlineMenuCipherData: InlineMenuCipherData[] = [];
|
||||
let inlineMenuCipherData: InlineMenuCipherData[];
|
||||
|
||||
if (this.showInlineMenuAccountCreation()) {
|
||||
inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers(
|
||||
@@ -526,10 +538,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
};
|
||||
|
||||
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
|
||||
void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url);
|
||||
void BrowserApi.tabSendMessage(pageDetails.tab, {
|
||||
this.buildSubFrameOffsets(
|
||||
pageDetails.tab,
|
||||
pageDetails.frameId,
|
||||
pageDetails.details.url,
|
||||
).catch((error) => this.logService.error(error));
|
||||
BrowserApi.tabSendMessage(pageDetails.tab, {
|
||||
command: "setupRebuildSubFrameOffsetsListeners",
|
||||
});
|
||||
}).catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
const pageDetailsMap = this.pageDetailsForTab[sender.tab.id];
|
||||
@@ -619,11 +635,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
if (!subFrameOffset) {
|
||||
subFrameOffsetsForTab.set(frameId, null);
|
||||
void BrowserApi.tabSendMessage(
|
||||
BrowserApi.tabSendMessage(
|
||||
tab,
|
||||
{ command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId },
|
||||
{ frameId },
|
||||
);
|
||||
).catch((error) => this.logService.error(error));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -655,11 +671,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
frameId,
|
||||
);
|
||||
|
||||
void BrowserApi.tabSendMessage(
|
||||
BrowserApi.tabSendMessage(
|
||||
tab,
|
||||
{ command: "destroyAutofillInlineMenuListeners" },
|
||||
{ frameId },
|
||||
);
|
||||
).catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -695,13 +711,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
}
|
||||
|
||||
if (!this.checkIsInlineMenuButtonVisible()) {
|
||||
void this.toggleInlineMenuHidden(
|
||||
this.toggleInlineMenuHidden(
|
||||
{ isInlineMenuHidden: false, setTransparentInlineMenu: true },
|
||||
sender,
|
||||
);
|
||||
).catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender);
|
||||
this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender).catch(
|
||||
(error) => this.logService.error(error),
|
||||
);
|
||||
|
||||
const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage(
|
||||
sender.tab,
|
||||
@@ -721,7 +739,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender);
|
||||
this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender).catch(
|
||||
(error) => this.logService.error(error),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -806,7 +826,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
const command = "closeAutofillInlineMenu";
|
||||
const sendOptions = { frameId: 0 };
|
||||
if (forceCloseInlineMenu) {
|
||||
void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions);
|
||||
BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch(
|
||||
(error) => this.logService.error(error),
|
||||
);
|
||||
this.isInlineMenuButtonVisible = false;
|
||||
this.isInlineMenuListVisible = false;
|
||||
return;
|
||||
@@ -817,11 +839,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
}
|
||||
|
||||
if (this.isFieldCurrentlyFilling) {
|
||||
void BrowserApi.tabSendMessage(
|
||||
BrowserApi.tabSendMessage(
|
||||
sender.tab,
|
||||
{ command, overlayElement: AutofillOverlayElement.List },
|
||||
sendOptions,
|
||||
);
|
||||
).catch((error) => this.logService.error(error));
|
||||
this.isInlineMenuListVisible = false;
|
||||
return;
|
||||
}
|
||||
@@ -839,7 +861,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
this.isInlineMenuListVisible = false;
|
||||
}
|
||||
|
||||
void BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions);
|
||||
BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1090,23 +1114,34 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
{ focusedFieldData }: OverlayBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
if (this.focusedFieldData?.frameId && this.focusedFieldData.frameId !== sender.frameId) {
|
||||
void BrowserApi.tabSendMessage(
|
||||
if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) {
|
||||
BrowserApi.tabSendMessage(
|
||||
sender.tab,
|
||||
{ command: "unsetMostRecentlyFocusedField" },
|
||||
{ frameId: this.focusedFieldData.frameId },
|
||||
);
|
||||
).catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
const previousFocusedFieldData = this.focusedFieldData;
|
||||
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId };
|
||||
this.isFieldCurrentlyFocused = true;
|
||||
|
||||
const accountCreationFieldBlurred =
|
||||
previousFocusedFieldData?.showInlineMenuAccountCreation &&
|
||||
!this.focusedFieldData.showInlineMenuAccountCreation;
|
||||
|
||||
if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) {
|
||||
void this.updateIdentityCiphersOnLoginField(previousFocusedFieldData);
|
||||
this.updateIdentityCiphersOnLoginField(previousFocusedFieldData).catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousFocusedFieldData?.filledByCipherType !== focusedFieldData?.filledByCipherType) {
|
||||
const updateAllCipherTypes = focusedFieldData.filledByCipherType !== CipherType.Login;
|
||||
this.updateOverlayCiphers(updateAllCipherTypes).catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,9 +1388,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
void BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", {
|
||||
BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", {
|
||||
direction,
|
||||
});
|
||||
}).catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1373,13 +1408,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
void BrowserApi.tabSendMessage(
|
||||
sender.tab,
|
||||
{ command: "addNewVaultItemFromOverlay", addNewCipherType },
|
||||
{
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
);
|
||||
this.currentAddNewItemData = { addNewCipherType, sender };
|
||||
BrowserApi.tabSendMessage(sender.tab, {
|
||||
command: "addNewVaultItemFromOverlay",
|
||||
addNewCipherType,
|
||||
}).catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1396,18 +1429,154 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
{ addNewCipherType, login, card, identity }: OverlayAddNewItemMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
if (!addNewCipherType) {
|
||||
if (
|
||||
!this.currentAddNewItemData ||
|
||||
sender.tab.id !== this.currentAddNewItemData.sender.tab.id ||
|
||||
!addNewCipherType ||
|
||||
this.currentAddNewItemData.addNewCipherType !== addNewCipherType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (login && this.isAddingNewLogin()) {
|
||||
this.updateCurrentAddNewItemLogin(login);
|
||||
}
|
||||
|
||||
if (card && this.isAddingNewCard()) {
|
||||
this.updateCurrentAddNewItemCard(card);
|
||||
}
|
||||
|
||||
if (identity && this.isAddingNewIdentity()) {
|
||||
this.updateCurrentAddNewItemIdentity(identity);
|
||||
}
|
||||
|
||||
this.addNewVaultItemSubject.next(this.currentAddNewItemData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the current add new item data is for adding a new login.
|
||||
*/
|
||||
private isAddingNewLogin() {
|
||||
return this.currentAddNewItemData.addNewCipherType === CipherType.Login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the current add new item data is for adding a new card.
|
||||
*/
|
||||
private isAddingNewCard() {
|
||||
return this.currentAddNewItemData.addNewCipherType === CipherType.Card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the current add new item data is for adding a new identity.
|
||||
*/
|
||||
private isAddingNewIdentity() {
|
||||
return this.currentAddNewItemData.addNewCipherType === CipherType.Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current add new item data with the provided login data. If the
|
||||
* login data is already present, the data will be merged with the existing data.
|
||||
*
|
||||
* @param login - The login data captured from the extension message
|
||||
*/
|
||||
private updateCurrentAddNewItemLogin(login: NewLoginCipherData) {
|
||||
if (!this.currentAddNewItemData.login) {
|
||||
this.currentAddNewItemData.login = login;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLoginData = this.currentAddNewItemData.login;
|
||||
this.currentAddNewItemData.login = {
|
||||
uri: login.uri || currentLoginData.uri,
|
||||
hostname: login.hostname || currentLoginData.hostname,
|
||||
username: login.username || currentLoginData.username,
|
||||
password: login.password || currentLoginData.password,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current add new item data with the provided card data. If the
|
||||
* card data is already present, the data will be merged with the existing data.
|
||||
*
|
||||
* @param card - The card data captured from the extension message
|
||||
*/
|
||||
private updateCurrentAddNewItemCard(card: NewCardCipherData) {
|
||||
if (!this.currentAddNewItemData.card) {
|
||||
this.currentAddNewItemData.card = card;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCardData = this.currentAddNewItemData.card;
|
||||
this.currentAddNewItemData.card = {
|
||||
cardholderName: card.cardholderName || currentCardData.cardholderName,
|
||||
number: card.number || currentCardData.number,
|
||||
expirationMonth: card.expirationMonth || currentCardData.expirationMonth,
|
||||
expirationYear: card.expirationYear || currentCardData.expirationYear,
|
||||
expirationDate: card.expirationDate || currentCardData.expirationDate,
|
||||
cvv: card.cvv || currentCardData.cvv,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current add new item data with the provided identity data. If the
|
||||
* identity data is already present, the data will be merged with the existing data.
|
||||
*
|
||||
* @param identity - The identity data captured from the extension message
|
||||
*/
|
||||
private updateCurrentAddNewItemIdentity(identity: NewIdentityCipherData) {
|
||||
if (!this.currentAddNewItemData.identity) {
|
||||
this.currentAddNewItemData.identity = identity;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIdentityData = this.currentAddNewItemData.identity;
|
||||
this.currentAddNewItemData.identity = {
|
||||
title: identity.title || currentIdentityData.title,
|
||||
firstName: identity.firstName || currentIdentityData.firstName,
|
||||
middleName: identity.middleName || currentIdentityData.middleName,
|
||||
lastName: identity.lastName || currentIdentityData.lastName,
|
||||
fullName: identity.fullName || currentIdentityData.fullName,
|
||||
address1: identity.address1 || currentIdentityData.address1,
|
||||
address2: identity.address2 || currentIdentityData.address2,
|
||||
address3: identity.address3 || currentIdentityData.address3,
|
||||
city: identity.city || currentIdentityData.city,
|
||||
state: identity.state || currentIdentityData.state,
|
||||
postalCode: identity.postalCode || currentIdentityData.postalCode,
|
||||
country: identity.country || currentIdentityData.country,
|
||||
company: identity.company || currentIdentityData.company,
|
||||
phone: identity.phone || currentIdentityData.phone,
|
||||
email: identity.email || currentIdentityData.email,
|
||||
username: identity.username || currentIdentityData.username,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles building a new cipher and opening the add/edit vault item popout.
|
||||
*
|
||||
* @param login - The login data captured from the extension message
|
||||
* @param card - The card data captured from the extension message
|
||||
* @param identity - The identity data captured from the extension message
|
||||
* @param sender - The sender of the extension message
|
||||
*/
|
||||
private async buildCipherAndOpenAddEditVaultItemPopout({
|
||||
login,
|
||||
card,
|
||||
identity,
|
||||
sender,
|
||||
}: CurrentAddNewItemData) {
|
||||
const cipherView: CipherView = this.buildNewVaultItemCipherView({
|
||||
addNewCipherType,
|
||||
login,
|
||||
card,
|
||||
identity,
|
||||
});
|
||||
|
||||
if (cipherView) {
|
||||
if (!cipherView) {
|
||||
this.currentAddNewItemData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.closeInlineMenu(sender);
|
||||
await this.cipherService.setAddEditCipherInfo({
|
||||
cipher: cipherView,
|
||||
@@ -1416,32 +1585,30 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
} catch (error) {
|
||||
this.logService.error("Error building cipher and opening add/edit vault item popout", error);
|
||||
}
|
||||
|
||||
this.currentAddNewItemData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a new cipher view with the provided vault item data.
|
||||
*
|
||||
* @param addNewCipherType - The type of cipher to add
|
||||
* @param login - The login data captured from the extension message
|
||||
* @param card - The card data captured from the extension message
|
||||
* @param identity - The identity data captured from the extension message
|
||||
*/
|
||||
private buildNewVaultItemCipherView({
|
||||
addNewCipherType,
|
||||
login,
|
||||
card,
|
||||
identity,
|
||||
}: OverlayAddNewItemMessage) {
|
||||
if (login && addNewCipherType === CipherType.Login) {
|
||||
private buildNewVaultItemCipherView({ login, card, identity }: OverlayAddNewItemMessage) {
|
||||
if (login && this.isAddingNewLogin()) {
|
||||
return this.buildLoginCipherView(login);
|
||||
}
|
||||
|
||||
if (card && addNewCipherType === CipherType.Card) {
|
||||
if (card && this.isAddingNewCard()) {
|
||||
return this.buildCardCipherView(card);
|
||||
}
|
||||
|
||||
if (identity && addNewCipherType === CipherType.Identity) {
|
||||
if (identity && this.isAddingNewIdentity()) {
|
||||
return this.buildIdentityCipherView(identity);
|
||||
}
|
||||
}
|
||||
@@ -1558,8 +1725,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* Updates the property that identifies if a form field set up for the inline menu is currently focused.
|
||||
*
|
||||
* @param message - The message received from the web page
|
||||
* @param sender - The sender of the port message
|
||||
*/
|
||||
private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) {
|
||||
private updateIsFieldCurrentlyFocused(
|
||||
message: OverlayBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused;
|
||||
}
|
||||
|
||||
@@ -1651,7 +1826,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.focusedFieldData?.frameId === sender.frameId) {
|
||||
if (this.senderFrameHasFocusedField(sender)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1676,6 +1851,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
return sender.tab.id === this.focusedFieldData?.tabId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the sender frame is the same as the focused field's frame.
|
||||
*
|
||||
* @param sender - The sender of the message
|
||||
*/
|
||||
private senderFrameHasFocusedField(sender: chrome.runtime.MessageSender) {
|
||||
return sender.frameId === this.focusedFieldData?.frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu
|
||||
* if the focused field is within the viewport.
|
||||
@@ -1689,7 +1873,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
this.resetFocusedFieldSubFrameOffsets(sender);
|
||||
this.cancelInlineMenuFadeInAndPositionUpdate();
|
||||
void this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender);
|
||||
this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender).catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
this.repositionInlineMenuSubject.next(sender);
|
||||
}
|
||||
|
||||
@@ -1879,14 +2065,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
filledByCipherType: this.focusedFieldData?.filledByCipherType,
|
||||
showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(),
|
||||
});
|
||||
void this.updateInlineMenuPosition(
|
||||
this.updateInlineMenuPosition(
|
||||
{
|
||||
overlayElement: isInlineMenuListPort
|
||||
? AutofillOverlayElement.List
|
||||
: AutofillOverlayElement.Button,
|
||||
},
|
||||
port.sender,
|
||||
);
|
||||
).catch((error) => this.logService.error(error));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,10 +61,13 @@ describe("AutofillInit", () => {
|
||||
autofillInit.init();
|
||||
jest.advanceTimersByTime(250);
|
||||
|
||||
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
command: "bgCollectPageDetails",
|
||||
sender: "autofillInit",
|
||||
});
|
||||
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
command: "bgCollectPageDetails",
|
||||
sender: "autofillInit",
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service";
|
||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../vault/fido2/browser-fido2-user-interface.service";
|
||||
import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
|
||||
|
||||
@Component({
|
||||
selector: "app-fido2-use-browser-link",
|
||||
@@ -29,13 +29,13 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { ZonedMessageListenerService } from "../../../../platform/browser/zoned-message-listener.service";
|
||||
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
|
||||
import {
|
||||
BrowserFido2Message,
|
||||
BrowserFido2UserInterfaceSession,
|
||||
} from "../../../fido2/browser-fido2-user-interface.service";
|
||||
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
|
||||
import { VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
} from "../../../vault/fido2/browser-fido2-user-interface.service";
|
||||
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
|
||||
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
|
||||
|
||||
interface ViewData {
|
||||
message: BrowserFido2Message;
|
||||
@@ -76,6 +76,11 @@ export class AutoFillConstants {
|
||||
"textarea",
|
||||
...AutoFillConstants.ExcludedAutofillTypes,
|
||||
];
|
||||
|
||||
static readonly ExcludedIdentityAutocompleteTypes: Set<string> = new Set([
|
||||
"current-password",
|
||||
"new-password",
|
||||
]);
|
||||
}
|
||||
|
||||
export class CreditCardAutoFillConstants {
|
||||
|
||||
@@ -37,10 +37,9 @@ describe("AutofillOverlayContentService", () => {
|
||||
);
|
||||
autofillInit = new AutofillInit(autofillOverlayContentService);
|
||||
autofillInit.init();
|
||||
sendExtensionMessageSpy = jest.spyOn(
|
||||
autofillOverlayContentService as any,
|
||||
"sendExtensionMessage",
|
||||
);
|
||||
sendExtensionMessageSpy = jest
|
||||
.spyOn(autofillOverlayContentService as any, "sendExtensionMessage")
|
||||
.mockResolvedValue(undefined);
|
||||
Object.defineProperty(document, "readyState", {
|
||||
value: defaultWindowReadyState,
|
||||
writable: true,
|
||||
@@ -1099,7 +1098,9 @@ describe("AutofillOverlayContentService", () => {
|
||||
selectFieldElement.dispatchEvent(new Event("focus"));
|
||||
await flushPromises();
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu", {
|
||||
forceCloseInlineMenu: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the most recently focused field", async () => {
|
||||
@@ -1986,6 +1987,19 @@ describe("AutofillOverlayContentService", () => {
|
||||
expect(autofillFieldFocusSpy).not.toHaveBeenCalled();
|
||||
expect(nextFocusableElement.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("focuses the most recently focused input field if no other tabbable elements are found", async () => {
|
||||
autofillOverlayContentService["focusableElements"] = [];
|
||||
findTabsSpy.mockReturnValue([]);
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "redirectAutofillInlineMenuFocusOut",
|
||||
data: { direction: RedirectFocusDirection.Next },
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(autofillFieldFocusSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateAutofillInlineMenuVisibility message handler", () => {
|
||||
|
||||
@@ -249,10 +249,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
* to the background script to add a new cipher.
|
||||
*/
|
||||
async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
|
||||
if (!(await this.isInlineMenuListVisible())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = "autofillOverlayAddNewVaultItem";
|
||||
|
||||
if (addNewCipherType === CipherType.Login) {
|
||||
@@ -338,7 +334,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
|
||||
const indexOffset = direction === RedirectFocusDirection.Previous ? -1 : 1;
|
||||
const redirectFocusElement = this.focusableElements[focusedElementIndex + indexOffset];
|
||||
redirectFocusElement?.focus();
|
||||
if (redirectFocusElement) {
|
||||
redirectFocusElement.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusMostRecentlyFocusedField();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -675,7 +676,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
}
|
||||
|
||||
if (elementIsSelectElement(formFieldElement)) {
|
||||
await this.sendExtensionMessage("closeAutofillInlineMenu");
|
||||
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||
forceCloseInlineMenu: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -758,7 +761,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
private async updateMostRecentlyFocusedField(
|
||||
formFieldElement: ElementWithOpId<FormFieldElement>,
|
||||
) {
|
||||
if (!formFieldElement || !elementIsFillableFormField(formFieldElement)) {
|
||||
if (
|
||||
!formFieldElement ||
|
||||
!elementIsFillableFormField(formFieldElement) ||
|
||||
elementIsSelectElement(formFieldElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1418,8 +1425,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
focusedFieldRectsTop + this.focusedFieldData?.focusedFieldRects?.height;
|
||||
const viewportHeight = globalThis.innerHeight + globalThis.scrollY;
|
||||
return (
|
||||
focusedFieldRectsTop &&
|
||||
focusedFieldRectsTop > 0 &&
|
||||
!globalThis.isNaN(focusedFieldRectsTop) &&
|
||||
focusedFieldRectsTop >= 0 &&
|
||||
focusedFieldRectsTop < viewportHeight &&
|
||||
focusedFieldRectsBottom < viewportHeight
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ import {
|
||||
GenerateFillScriptOptions,
|
||||
PageDetail,
|
||||
} from "./abstractions/autofill.service";
|
||||
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
|
||||
import { AutoFillConstants } from "./autofill-constants";
|
||||
import AutofillService from "./autofill.service";
|
||||
|
||||
const mockEquivalentDomains = [
|
||||
@@ -3056,12 +3056,12 @@ describe("AutofillService", () => {
|
||||
options.cipher.identity = mock<IdentityView>();
|
||||
});
|
||||
|
||||
it("returns null if an identify is not found within the cipher", () => {
|
||||
it("returns null if an identify is not found within the cipher", async () => {
|
||||
options.cipher.identity = null;
|
||||
jest.spyOn(autofillService as any, "makeScriptAction");
|
||||
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
@@ -3087,432 +3087,389 @@ describe("AutofillService", () => {
|
||||
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
||||
});
|
||||
|
||||
it("will not attempt to match custom fields", () => {
|
||||
const customField = createAutofillFieldMock({ tagName: "span" });
|
||||
pageDetails.fields.push(customField);
|
||||
let isRefactorFeatureFlagSet = false;
|
||||
for (let index = 0; index < 2; index++) {
|
||||
describe(`when the isRefactorFeatureFlagSet is ${isRefactorFeatureFlagSet}`, () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag.mockResolvedValue(isRefactorFeatureFlagSet);
|
||||
});
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
afterAll(() => {
|
||||
isRefactorFeatureFlagSet = true;
|
||||
});
|
||||
|
||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
|
||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||
expect(value.script).toStrictEqual([]);
|
||||
});
|
||||
it("will not attempt to match custom fields", async () => {
|
||||
const customField = createAutofillFieldMock({ tagName: "span" });
|
||||
pageDetails.fields.push(customField);
|
||||
|
||||
it("will not attempt to match a field that is of an excluded type", () => {
|
||||
const excludedField = createAutofillFieldMock({ type: "hidden" });
|
||||
pageDetails.fields.push(excludedField);
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
|
||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||
expect(value.script).toStrictEqual([]);
|
||||
});
|
||||
|
||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
|
||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
|
||||
excludedField,
|
||||
AutoFillConstants.ExcludedAutofillTypes,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||
expect(value.script).toStrictEqual([]);
|
||||
});
|
||||
it("will not attempt to match a field that is of an excluded type", async () => {
|
||||
const excludedField = createAutofillFieldMock({ type: "hidden" });
|
||||
pageDetails.fields.push(excludedField);
|
||||
|
||||
it("will not attempt to match a field that is not viewable", () => {
|
||||
const viewableField = createAutofillFieldMock({ viewable: false });
|
||||
pageDetails.fields.push(viewableField);
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
|
||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
|
||||
excludedField,
|
||||
AutoFillConstants.ExcludedAutofillTypes,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||
expect(value.script).toStrictEqual([]);
|
||||
});
|
||||
|
||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
|
||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||
expect(value.script).toStrictEqual([]);
|
||||
});
|
||||
it("will not attempt to match a field that is not viewable", async () => {
|
||||
const viewableField = createAutofillFieldMock({ viewable: false });
|
||||
pageDetails.fields.push(viewableField);
|
||||
|
||||
it("will match a full name field to the vault item identity value", () => {
|
||||
const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" });
|
||||
pageDetails.fields = [fullNameField];
|
||||
options.cipher.identity.firstName = firstName;
|
||||
options.cipher.identity.middleName = middleName;
|
||||
options.cipher.identity.lastName = lastName;
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
|
||||
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
|
||||
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
|
||||
expect(value.script).toStrictEqual([]);
|
||||
});
|
||||
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
fullNameField.htmlName,
|
||||
IdentityAutoFillConstants.FullNameFieldNames,
|
||||
IdentityAutoFillConstants.FullNameFieldNameValues,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
`${firstName} ${middleName} ${lastName}`,
|
||||
fullNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual([
|
||||
"fill_by_opid",
|
||||
fullNameField.opid,
|
||||
`${firstName} ${middleName} ${lastName}`,
|
||||
]);
|
||||
});
|
||||
it("will match a full name field to the vault item identity value", async () => {
|
||||
const fullNameField = createAutofillFieldMock({
|
||||
opid: "fullName",
|
||||
htmlName: "full-name",
|
||||
});
|
||||
pageDetails.fields = [fullNameField];
|
||||
options.cipher.identity.firstName = firstName;
|
||||
options.cipher.identity.middleName = middleName;
|
||||
options.cipher.identity.lastName = lastName;
|
||||
|
||||
it("will match a full name field to the a vault item that only has a last name", () => {
|
||||
const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" });
|
||||
pageDetails.fields = [fullNameField];
|
||||
options.cipher.identity.firstName = "";
|
||||
options.cipher.identity.middleName = "";
|
||||
options.cipher.identity.lastName = lastName;
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
`${firstName} ${middleName} ${lastName}`,
|
||||
fullNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual([
|
||||
"fill_by_opid",
|
||||
fullNameField.opid,
|
||||
`${firstName} ${middleName} ${lastName}`,
|
||||
]);
|
||||
});
|
||||
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
fullNameField.htmlName,
|
||||
IdentityAutoFillConstants.FullNameFieldNames,
|
||||
IdentityAutoFillConstants.FullNameFieldNameValues,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
lastName,
|
||||
fullNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]);
|
||||
});
|
||||
it("will match a full name field to the a vault item that only has a last name", async () => {
|
||||
const fullNameField = createAutofillFieldMock({
|
||||
opid: "fullName",
|
||||
htmlName: "full-name",
|
||||
});
|
||||
pageDetails.fields = [fullNameField];
|
||||
options.cipher.identity.firstName = "";
|
||||
options.cipher.identity.middleName = "";
|
||||
options.cipher.identity.lastName = lastName;
|
||||
|
||||
it("will match first name, middle name, and last name fields to the vault item identity value", () => {
|
||||
const firstNameField = createAutofillFieldMock({
|
||||
opid: "firstName",
|
||||
htmlName: "first-name",
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
lastName,
|
||||
fullNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]);
|
||||
});
|
||||
|
||||
it("will match first name, middle name, and last name fields to the vault item identity value", async () => {
|
||||
const firstNameField = createAutofillFieldMock({
|
||||
opid: "firstName",
|
||||
htmlName: "first-name",
|
||||
});
|
||||
const middleNameField = createAutofillFieldMock({
|
||||
opid: "middleName",
|
||||
htmlName: "middle-name",
|
||||
});
|
||||
const lastNameField = createAutofillFieldMock({
|
||||
opid: "lastName",
|
||||
htmlName: "last-name",
|
||||
});
|
||||
pageDetails.fields = [firstNameField, middleNameField, lastNameField];
|
||||
options.cipher.identity.firstName = firstName;
|
||||
options.cipher.identity.middleName = middleName;
|
||||
options.cipher.identity.lastName = lastName;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity.firstName,
|
||||
firstNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity.middleName,
|
||||
middleNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity.lastName,
|
||||
lastNameField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]);
|
||||
expect(value.script[5]).toStrictEqual([
|
||||
"fill_by_opid",
|
||||
middleNameField.opid,
|
||||
middleName,
|
||||
]);
|
||||
expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]);
|
||||
});
|
||||
|
||||
it("will match title and email fields to the vault item identity value", async () => {
|
||||
const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" });
|
||||
const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" });
|
||||
pageDetails.fields = [titleField, emailField];
|
||||
const title = "Mr.";
|
||||
const email = "email@example.com";
|
||||
options.cipher.identity.title = title;
|
||||
options.cipher.identity.email = email;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity.title,
|
||||
titleField,
|
||||
filledFields,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity.email,
|
||||
emailField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]);
|
||||
expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]);
|
||||
});
|
||||
|
||||
it("will match a full address field to the vault item identity values", async () => {
|
||||
const fullAddressField = createAutofillFieldMock({
|
||||
opid: "fullAddress",
|
||||
htmlName: "address",
|
||||
});
|
||||
pageDetails.fields = [fullAddressField];
|
||||
const address1 = "123 Main St.";
|
||||
const address2 = "Apt. 1";
|
||||
const address3 = "P.O. Box 123";
|
||||
options.cipher.identity.address1 = address1;
|
||||
options.cipher.identity.address2 = address2;
|
||||
options.cipher.identity.address3 = address3;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
`${address1}, ${address2}, ${address3}`,
|
||||
fullAddressField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual([
|
||||
"fill_by_opid",
|
||||
fullAddressField.opid,
|
||||
`${address1}, ${address2}, ${address3}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", async () => {
|
||||
const address1Field = createAutofillFieldMock({
|
||||
opid: "address1",
|
||||
htmlName: "address-1",
|
||||
});
|
||||
const address2Field = createAutofillFieldMock({
|
||||
opid: "address2",
|
||||
htmlName: "address-2",
|
||||
});
|
||||
const address3Field = createAutofillFieldMock({
|
||||
opid: "address3",
|
||||
htmlName: "address-3",
|
||||
});
|
||||
const postalCodeField = createAutofillFieldMock({
|
||||
opid: "postalCode",
|
||||
htmlName: "postal-code",
|
||||
});
|
||||
const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" });
|
||||
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
|
||||
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
|
||||
const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" });
|
||||
const usernameField = createAutofillFieldMock({
|
||||
opid: "username",
|
||||
htmlName: "username",
|
||||
});
|
||||
const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" });
|
||||
pageDetails.fields = [
|
||||
address1Field,
|
||||
address2Field,
|
||||
address3Field,
|
||||
postalCodeField,
|
||||
cityField,
|
||||
stateField,
|
||||
countryField,
|
||||
phoneField,
|
||||
usernameField,
|
||||
companyField,
|
||||
];
|
||||
const address1 = "123 Main St.";
|
||||
const address2 = "Apt. 1";
|
||||
const address3 = "P.O. Box 123";
|
||||
const postalCode = "12345";
|
||||
const city = "City";
|
||||
const state = "TX";
|
||||
const country = "US";
|
||||
const phone = "123-456-7890";
|
||||
const username = "username";
|
||||
const company = "Company";
|
||||
options.cipher.identity.address1 = address1;
|
||||
options.cipher.identity.address2 = address2;
|
||||
options.cipher.identity.address3 = address3;
|
||||
options.cipher.identity.postalCode = postalCode;
|
||||
options.cipher.identity.city = city;
|
||||
options.cipher.identity.state = state;
|
||||
options.cipher.identity.country = country;
|
||||
options.cipher.identity.phone = phone;
|
||||
options.cipher.identity.username = username;
|
||||
options.cipher.identity.company = company;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(value.script).toContainEqual(["fill_by_opid", address1Field.opid, address1]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", address2Field.opid, address2]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", address3Field.opid, address3]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", postalCodeField.opid, postalCode]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", cityField.opid, city]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", stateField.opid, state]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", countryField.opid, country]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", phoneField.opid, phone]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", usernameField.opid, username]);
|
||||
expect(value.script).toContainEqual(["fill_by_opid", companyField.opid, company]);
|
||||
});
|
||||
|
||||
it("will find the two character IsoState value for an identity cipher that contains the full name of a state", async () => {
|
||||
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
|
||||
pageDetails.fields = [stateField];
|
||||
const state = "California";
|
||||
options.cipher.identity.state = state;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
"CA",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]);
|
||||
});
|
||||
|
||||
it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", async () => {
|
||||
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
|
||||
pageDetails.fields = [stateField];
|
||||
const state = "Ontario";
|
||||
options.cipher.identity.state = state;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
"ON",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]);
|
||||
});
|
||||
|
||||
it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", async () => {
|
||||
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
|
||||
pageDetails.fields = [countryField];
|
||||
const country = "Somalia";
|
||||
options.cipher.identity.country = country;
|
||||
|
||||
const value = await autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
"SO",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]);
|
||||
});
|
||||
});
|
||||
const middleNameField = createAutofillFieldMock({
|
||||
opid: "middleName",
|
||||
htmlName: "middle-name",
|
||||
});
|
||||
const lastNameField = createAutofillFieldMock({ opid: "lastName", htmlName: "last-name" });
|
||||
pageDetails.fields = [firstNameField, middleNameField, lastNameField];
|
||||
options.cipher.identity.firstName = firstName;
|
||||
options.cipher.identity.middleName = middleName;
|
||||
options.cipher.identity.lastName = lastName;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
firstNameField.htmlName,
|
||||
IdentityAutoFillConstants.FirstnameFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
middleNameField.htmlName,
|
||||
IdentityAutoFillConstants.MiddlenameFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
lastNameField.htmlName,
|
||||
IdentityAutoFillConstants.LastnameFieldNames,
|
||||
);
|
||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity,
|
||||
expect.anything(),
|
||||
filledFields,
|
||||
firstNameField.opid,
|
||||
);
|
||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity,
|
||||
expect.anything(),
|
||||
filledFields,
|
||||
middleNameField.opid,
|
||||
);
|
||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity,
|
||||
expect.anything(),
|
||||
filledFields,
|
||||
lastNameField.opid,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]);
|
||||
expect(value.script[5]).toStrictEqual(["fill_by_opid", middleNameField.opid, middleName]);
|
||||
expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]);
|
||||
});
|
||||
|
||||
it("will match title and email fields to the vault item identity value", () => {
|
||||
const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" });
|
||||
const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" });
|
||||
pageDetails.fields = [titleField, emailField];
|
||||
const title = "Mr.";
|
||||
const email = "email@example.com";
|
||||
options.cipher.identity.title = title;
|
||||
options.cipher.identity.email = email;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
titleField.htmlName,
|
||||
IdentityAutoFillConstants.TitleFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
emailField.htmlName,
|
||||
IdentityAutoFillConstants.EmailFieldNames,
|
||||
);
|
||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity,
|
||||
expect.anything(),
|
||||
filledFields,
|
||||
titleField.opid,
|
||||
);
|
||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
options.cipher.identity,
|
||||
expect.anything(),
|
||||
filledFields,
|
||||
emailField.opid,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]);
|
||||
expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]);
|
||||
});
|
||||
|
||||
it("will match a full address field to the vault item identity values", () => {
|
||||
const fullAddressField = createAutofillFieldMock({
|
||||
opid: "fullAddress",
|
||||
htmlName: "address",
|
||||
});
|
||||
pageDetails.fields = [fullAddressField];
|
||||
const address1 = "123 Main St.";
|
||||
const address2 = "Apt. 1";
|
||||
const address3 = "P.O. Box 123";
|
||||
options.cipher.identity.address1 = address1;
|
||||
options.cipher.identity.address2 = address2;
|
||||
options.cipher.identity.address3 = address3;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
fullAddressField.htmlName,
|
||||
IdentityAutoFillConstants.AddressFieldNames,
|
||||
IdentityAutoFillConstants.AddressFieldNameValues,
|
||||
);
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
`${address1}, ${address2}, ${address3}`,
|
||||
fullAddressField,
|
||||
filledFields,
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual([
|
||||
"fill_by_opid",
|
||||
fullAddressField.opid,
|
||||
`${address1}, ${address2}, ${address3}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", () => {
|
||||
const address1Field = createAutofillFieldMock({ opid: "address1", htmlName: "address-1" });
|
||||
const address2Field = createAutofillFieldMock({ opid: "address2", htmlName: "address-2" });
|
||||
const address3Field = createAutofillFieldMock({ opid: "address3", htmlName: "address-3" });
|
||||
const postalCodeField = createAutofillFieldMock({
|
||||
opid: "postalCode",
|
||||
htmlName: "postal-code",
|
||||
});
|
||||
const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" });
|
||||
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
|
||||
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
|
||||
const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" });
|
||||
const usernameField = createAutofillFieldMock({ opid: "username", htmlName: "username" });
|
||||
const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" });
|
||||
pageDetails.fields = [
|
||||
address1Field,
|
||||
address2Field,
|
||||
address3Field,
|
||||
postalCodeField,
|
||||
cityField,
|
||||
stateField,
|
||||
countryField,
|
||||
phoneField,
|
||||
usernameField,
|
||||
companyField,
|
||||
];
|
||||
const address1 = "123 Main St.";
|
||||
const address2 = "Apt. 1";
|
||||
const address3 = "P.O. Box 123";
|
||||
const postalCode = "12345";
|
||||
const city = "City";
|
||||
const state = "State";
|
||||
const country = "Country";
|
||||
const phone = "123-456-7890";
|
||||
const username = "username";
|
||||
const company = "Company";
|
||||
options.cipher.identity.address1 = address1;
|
||||
options.cipher.identity.address2 = address2;
|
||||
options.cipher.identity.address3 = address3;
|
||||
options.cipher.identity.postalCode = postalCode;
|
||||
options.cipher.identity.city = city;
|
||||
options.cipher.identity.state = state;
|
||||
options.cipher.identity.country = country;
|
||||
options.cipher.identity.phone = phone;
|
||||
options.cipher.identity.username = username;
|
||||
options.cipher.identity.company = company;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
address1Field.htmlName,
|
||||
IdentityAutoFillConstants.Address1FieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
address2Field.htmlName,
|
||||
IdentityAutoFillConstants.Address2FieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
address3Field.htmlName,
|
||||
IdentityAutoFillConstants.Address3FieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
postalCodeField.htmlName,
|
||||
IdentityAutoFillConstants.PostalCodeFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
cityField.htmlName,
|
||||
IdentityAutoFillConstants.CityFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
stateField.htmlName,
|
||||
IdentityAutoFillConstants.StateFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
countryField.htmlName,
|
||||
IdentityAutoFillConstants.CountryFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
phoneField.htmlName,
|
||||
IdentityAutoFillConstants.PhoneFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
usernameField.htmlName,
|
||||
IdentityAutoFillConstants.UserNameFieldNames,
|
||||
);
|
||||
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
|
||||
companyField.htmlName,
|
||||
IdentityAutoFillConstants.CompanyFieldNames,
|
||||
);
|
||||
expect(autofillService["makeScriptAction"]).toHaveBeenCalled();
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", address1Field.opid, address1]);
|
||||
expect(value.script[5]).toStrictEqual(["fill_by_opid", address2Field.opid, address2]);
|
||||
expect(value.script[8]).toStrictEqual(["fill_by_opid", address3Field.opid, address3]);
|
||||
expect(value.script[11]).toStrictEqual(["fill_by_opid", cityField.opid, city]);
|
||||
expect(value.script[14]).toStrictEqual(["fill_by_opid", postalCodeField.opid, postalCode]);
|
||||
expect(value.script[17]).toStrictEqual(["fill_by_opid", companyField.opid, company]);
|
||||
expect(value.script[20]).toStrictEqual(["fill_by_opid", phoneField.opid, phone]);
|
||||
expect(value.script[23]).toStrictEqual(["fill_by_opid", usernameField.opid, username]);
|
||||
expect(value.script[26]).toStrictEqual(["fill_by_opid", stateField.opid, state]);
|
||||
expect(value.script[29]).toStrictEqual(["fill_by_opid", countryField.opid, country]);
|
||||
});
|
||||
|
||||
it("will find the two character IsoState value for an identity cipher that contains the full name of a state", () => {
|
||||
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
|
||||
pageDetails.fields = [stateField];
|
||||
const state = "California";
|
||||
options.cipher.identity.state = state;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
"CA",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]);
|
||||
});
|
||||
|
||||
it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", () => {
|
||||
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
|
||||
pageDetails.fields = [stateField];
|
||||
const state = "Ontario";
|
||||
options.cipher.identity.state = state;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
"ON",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]);
|
||||
});
|
||||
|
||||
it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", () => {
|
||||
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
|
||||
pageDetails.fields = [countryField];
|
||||
const country = "Somalia";
|
||||
options.cipher.identity.country = country;
|
||||
|
||||
const value = autofillService["generateIdentityFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
|
||||
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
|
||||
fillScript,
|
||||
"SO",
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||
@@ -478,6 +479,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return totpCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cipher requires password reprompt and opens the password reprompt popout if necessary.
|
||||
*
|
||||
* @param cipher - The cipher to autofill
|
||||
* @param tab - The tab to autofill
|
||||
*/
|
||||
async isPasswordRepromptRequired(cipher: CipherView, tab: chrome.tabs.Tab): Promise<boolean> {
|
||||
const userHasMasterPasswordAndKeyHash =
|
||||
await this.userVerificationService.hasMasterPasswordAndMasterKeyHash();
|
||||
@@ -654,7 +661,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
fillScript = this.generateIdentityFillScript(
|
||||
fillScript = await this.generateIdentityFillScript(
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
@@ -1243,12 +1250,16 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
* @returns {AutofillScript}
|
||||
* @private
|
||||
*/
|
||||
private generateIdentityFillScript(
|
||||
private async generateIdentityFillScript(
|
||||
fillScript: AutofillScript,
|
||||
pageDetails: AutofillPageDetails,
|
||||
filledFields: { [id: string]: AutofillField },
|
||||
options: GenerateFillScriptOptions,
|
||||
): AutofillScript {
|
||||
): Promise<AutofillScript> {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.GenerateIdentityFillScriptRefactor)) {
|
||||
return this._generateIdentityFillScript(fillScript, pageDetails, filledFields, options);
|
||||
}
|
||||
|
||||
if (!options.cipher.identity) {
|
||||
return null;
|
||||
}
|
||||
@@ -1476,6 +1487,589 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
return fillScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the autofill script for the specified page details and identity cipher item.
|
||||
*
|
||||
* @param fillScript - Object to store autofill script, passed between method references
|
||||
* @param pageDetails - The details of the page to autofill
|
||||
* @param filledFields - The fields that have already been filled, passed between method references
|
||||
* @param options - Contains data used to fill cipher items
|
||||
*/
|
||||
private _generateIdentityFillScript(
|
||||
fillScript: AutofillScript,
|
||||
pageDetails: AutofillPageDetails,
|
||||
filledFields: { [id: string]: AutofillField },
|
||||
options: GenerateFillScriptOptions,
|
||||
): AutofillScript {
|
||||
const identity = options.cipher.identity;
|
||||
if (!identity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let fieldsIndex = 0; fieldsIndex < pageDetails.fields.length; fieldsIndex++) {
|
||||
const field = pageDetails.fields[fieldsIndex];
|
||||
if (this.excludeFieldFromIdentityFill(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keywordsList = this.getIdentityAutofillFieldKeywords(field);
|
||||
const keywordsCombined = keywordsList.join(",");
|
||||
if (this.shouldMakeIdentityTitleFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.title, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityNameFillScript(filledFields, keywordsList)) {
|
||||
this.makeIdentityNameFillScript(fillScript, filledFields, field, identity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityFirstNameFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.firstName, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityMiddleNameFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.middleName, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityLastNameFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.lastName, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityEmailFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.email, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityAddressFillScript(filledFields, keywordsList)) {
|
||||
this.makeIdentityAddressFillScript(fillScript, filledFields, field, identity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityAddress1FillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.address1, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityAddress2FillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.address2, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityAddress3FillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.address3, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityPostalCodeFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.postalCode, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityCityFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.city, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityStateFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeIdentityStateFillScript(fillScript, filledFields, field, identity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityCountryFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeIdentityCountryFillScript(fillScript, filledFields, field, identity);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityPhoneFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.phone, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityUserNameFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.username, field, filledFields);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldMakeIdentityCompanyFillScript(filledFields, keywordsCombined)) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.company, field, filledFields);
|
||||
}
|
||||
}
|
||||
|
||||
return fillScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the current field should be excluded from triggering autofill of the identity cipher.
|
||||
*
|
||||
* @param field - The field to check
|
||||
*/
|
||||
private excludeFieldFromIdentityFill(field: AutofillField): boolean {
|
||||
return (
|
||||
AutofillService.isExcludedFieldType(field, AutoFillConstants.ExcludedAutofillTypes) ||
|
||||
AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) ||
|
||||
!field.viewable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers all unique keyword identifiers from a field that can be used to determine what
|
||||
* identity value should be filled.
|
||||
*
|
||||
* @param field - The field to gather keywords from
|
||||
*/
|
||||
private getIdentityAutofillFieldKeywords(field: AutofillField): string[] {
|
||||
const keywords: Set<string> = new Set();
|
||||
for (let index = 0; index < IdentityAutoFillConstants.IdentityAttributes.length; index++) {
|
||||
const attribute = IdentityAutoFillConstants.IdentityAttributes[index];
|
||||
if (field[attribute]) {
|
||||
keywords.add(
|
||||
field[attribute]
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-zA-Z0-9]+/g, ""),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(keywords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity title
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityTitleFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.title &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.TitleFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity name
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityNameFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string[],
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.name &&
|
||||
keywords.some((keyword) =>
|
||||
AutofillService.isFieldMatch(
|
||||
keyword,
|
||||
IdentityAutoFillConstants.FullNameFieldNames,
|
||||
IdentityAutoFillConstants.FullNameFieldNameValues,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity first name
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityFirstNameFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.firstName &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.FirstnameFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity middle name
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityMiddleNameFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.middleName &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.MiddlenameFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity last name
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityLastNameFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.lastName &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.LastnameFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity email
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityEmailFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.email &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.EmailFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity address
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityAddressFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string[],
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.address &&
|
||||
keywords.some((keyword) =>
|
||||
AutofillService.isFieldMatch(
|
||||
keyword,
|
||||
IdentityAutoFillConstants.AddressFieldNames,
|
||||
IdentityAutoFillConstants.AddressFieldNameValues,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity address1
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityAddress1FillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.address1 &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address1FieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity address2
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityAddress2FillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.address2 &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address2FieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity address3
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityAddress3FillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.address3 &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address3FieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity postal code
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityPostalCodeFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.postalCode &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PostalCodeFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity city
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityCityFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.city &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CityFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity state
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityStateFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.state &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.StateFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity country
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityCountryFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.country &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CountryFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity phone
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityPhoneFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.phone &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PhoneFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity username
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityUserNameFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.username &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.UserNameFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a fill script action for the identity company
|
||||
* field should be created for the provided field.
|
||||
*
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param keywords - The keywords from the field
|
||||
*/
|
||||
private shouldMakeIdentityCompanyFillScript(
|
||||
filledFields: Record<string, AutofillField>,
|
||||
keywords: string,
|
||||
): boolean {
|
||||
return (
|
||||
!filledFields.company &&
|
||||
AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CompanyFieldNames)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identity name fill script action for the provided field. This is used
|
||||
* when filling a `full name` field, using the first, middle, and last name from the
|
||||
* identity cipher item.
|
||||
*
|
||||
* @param fillScript - The autofill script to add the action to
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param field - The field to fill
|
||||
* @param identity - The identity cipher item
|
||||
*/
|
||||
private makeIdentityNameFillScript(
|
||||
fillScript: AutofillScript,
|
||||
filledFields: Record<string, AutofillField>,
|
||||
field: AutofillField,
|
||||
identity: IdentityView,
|
||||
) {
|
||||
let name = "";
|
||||
if (identity.firstName) {
|
||||
name += identity.firstName;
|
||||
}
|
||||
|
||||
if (identity.middleName) {
|
||||
name += !name ? identity.middleName : ` ${identity.middleName}`;
|
||||
}
|
||||
|
||||
if (identity.lastName) {
|
||||
name += !name ? identity.lastName : ` ${identity.lastName}`;
|
||||
}
|
||||
|
||||
this.makeScriptActionWithValue(fillScript, name, field, filledFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identity address fill script action for the provided field. This is used
|
||||
* when filling a generic `address` field, using the address1, address2, and address3
|
||||
* from the identity cipher item.
|
||||
*
|
||||
* @param fillScript - The autofill script to add the action to
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param field - The field to fill
|
||||
* @param identity - The identity cipher item
|
||||
*/
|
||||
private makeIdentityAddressFillScript(
|
||||
fillScript: AutofillScript,
|
||||
filledFields: Record<string, AutofillField>,
|
||||
field: AutofillField,
|
||||
identity: IdentityView,
|
||||
) {
|
||||
if (!identity.address1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let address = identity.address1;
|
||||
|
||||
if (identity.address2) {
|
||||
address += `, ${identity.address2}`;
|
||||
}
|
||||
|
||||
if (identity.address3) {
|
||||
address += `, ${identity.address3}`;
|
||||
}
|
||||
|
||||
this.makeScriptActionWithValue(fillScript, address, field, filledFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identity state fill script action for the provided field. This is used
|
||||
* when filling a `state` field, using the state value from the identity cipher item.
|
||||
* If the state value is a full name, it will be converted to an ISO code.
|
||||
*
|
||||
* @param fillScript - The autofill script to add the action to
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param field - The field to fill
|
||||
* @param identity - The identity cipher item
|
||||
*/
|
||||
private makeIdentityStateFillScript(
|
||||
fillScript: AutofillScript,
|
||||
filledFields: Record<string, AutofillField>,
|
||||
field: AutofillField,
|
||||
identity: IdentityView,
|
||||
) {
|
||||
if (!identity.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (identity.state.length <= 2) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.state, field, filledFields);
|
||||
return;
|
||||
}
|
||||
|
||||
const stateLower = identity.state.toLowerCase();
|
||||
const isoState =
|
||||
IdentityAutoFillConstants.IsoStates[stateLower] ||
|
||||
IdentityAutoFillConstants.IsoProvinces[stateLower];
|
||||
if (isoState) {
|
||||
this.makeScriptActionWithValue(fillScript, isoState, field, filledFields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identity country fill script action for the provided field. This is used
|
||||
* when filling a `country` field, using the country value from the identity cipher item.
|
||||
* If the country value is a full name, it will be converted to an ISO code.
|
||||
*
|
||||
* @param fillScript - The autofill script to add the action to
|
||||
* @param filledFields - The fields that have already been filled
|
||||
* @param field - The field to fill
|
||||
* @param identity - The identity cipher item
|
||||
*/
|
||||
private makeIdentityCountryFillScript(
|
||||
fillScript: AutofillScript,
|
||||
filledFields: Record<string, AutofillField>,
|
||||
field: AutofillField,
|
||||
identity: IdentityView,
|
||||
) {
|
||||
if (!identity.country) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (identity.country.length <= 2) {
|
||||
this.makeScriptActionWithValue(fillScript, identity.country, field, filledFields);
|
||||
return;
|
||||
}
|
||||
|
||||
const countryLower = identity.country.toLowerCase();
|
||||
const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower];
|
||||
if (isoCountry) {
|
||||
this.makeScriptActionWithValue(fillScript, isoCountry, field, filledFields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an HTMLInputElement type value and a list of
|
||||
* excluded types and returns true if the type is excluded.
|
||||
|
||||
@@ -16,10 +16,8 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
forms: {},
|
||||
fields: [],
|
||||
});
|
||||
chrome.runtime.sendMessage = jest.fn().mockImplementation((message) => ({
|
||||
result: message.command === "getInlineMenuFieldQualificationFeatureFlag",
|
||||
}));
|
||||
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||
inlineMenuFieldQualificationService["inlineMenuFieldQualificationFlagSet"] = true;
|
||||
});
|
||||
|
||||
describe("isFieldForLoginForm", () => {
|
||||
|
||||
@@ -209,10 +209,7 @@ export class InlineMenuFieldQualificationService
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, this.creditCardFieldKeywords)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, this.creditCardFieldKeywords);
|
||||
}
|
||||
|
||||
// If the field has a parent form, check the fields from that form exclusively
|
||||
@@ -232,10 +229,7 @@ export class InlineMenuFieldQualificationService
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords])
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords]);
|
||||
}
|
||||
|
||||
/** Validates the provided field as a field for an account creation form.
|
||||
@@ -264,10 +258,7 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
// If no password fields are found on the page, check for keywords that indicate the field is
|
||||
// part of an account creation form.
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords);
|
||||
}
|
||||
|
||||
// If the field has a parent form, check the fields from that form exclusively
|
||||
@@ -277,10 +268,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,9 +468,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardHolderFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
CreditCardAutoFillConstants.CardHolderFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -496,9 +485,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardNumberFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
CreditCardAutoFillConstants.CardNumberFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -514,9 +504,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardExpiryFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
CreditCardAutoFillConstants.CardExpiryFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -532,9 +523,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryMonthFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
CreditCardAutoFillConstants.ExpiryMonthFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -550,9 +542,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryYearFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
CreditCardAutoFillConstants.ExpiryYearFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -566,10 +559,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -584,10 +574,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.TitleFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.TitleFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -600,9 +587,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.FirstnameFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.FirstnameFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -616,9 +604,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.MiddlenameFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.MiddlenameFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -632,9 +621,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.LastnameFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.LastnameFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -648,9 +638,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.FullNameFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.FullNameFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -664,16 +655,13 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(
|
||||
field,
|
||||
[
|
||||
...IdentityAutoFillConstants.AddressFieldNames,
|
||||
...IdentityAutoFillConstants.Address1FieldNames,
|
||||
],
|
||||
false,
|
||||
)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
[
|
||||
...IdentityAutoFillConstants.AddressFieldNames,
|
||||
...IdentityAutoFillConstants.Address1FieldNames,
|
||||
],
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -687,9 +675,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address2FieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.Address2FieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -703,9 +692,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.Address3FieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.Address3FieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -719,10 +709,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CityFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CityFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -735,10 +722,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.StateFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.StateFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -751,9 +735,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PostalCodeFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.PostalCodeFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -767,10 +752,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CountryFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CountryFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -783,10 +765,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CompanyFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CompanyFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -799,10 +778,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PhoneFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PhoneFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -818,10 +794,7 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.EmailFieldNames, false)
|
||||
);
|
||||
return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.EmailFieldNames, false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -834,9 +807,10 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.UserNameFieldNames, false)
|
||||
return this.keywordsFoundInFieldData(
|
||||
field,
|
||||
IdentityAutoFillConstants.UserNameFieldNames,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1039,11 +1013,13 @@ export class InlineMenuFieldQualificationService
|
||||
fuzzyMatchKeywords = true,
|
||||
) {
|
||||
const searchedValues = this.getAutofillFieldDataKeywords(autofillFieldData, fuzzyMatchKeywords);
|
||||
const parsedKeywords = keywords.map((keyword) => keyword.replace(/-/g, ""));
|
||||
|
||||
if (typeof searchedValues === "string") {
|
||||
return keywords.some((keyword) => searchedValues.indexOf(keyword) > -1);
|
||||
return parsedKeywords.some((keyword) => searchedValues.indexOf(keyword) > -1);
|
||||
}
|
||||
|
||||
return keywords.some((keyword) => searchedValues.has(keyword));
|
||||
return parsedKeywords.some((keyword) => searchedValues.has(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1072,8 +1048,19 @@ export class InlineMenuFieldQualificationService
|
||||
autofillFieldData["label-tag"],
|
||||
autofillFieldData["label-top"],
|
||||
];
|
||||
const keywordsSet = new Set<string>(keywords);
|
||||
const stringValue = keywords.join(",").toLowerCase();
|
||||
const keywordsSet = new Set<string>();
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
if (typeof keywords[i] === "string") {
|
||||
keywords[i]
|
||||
.toLowerCase()
|
||||
.replace(/-/g, "")
|
||||
.replace(/[^a-zA-Z0-9]+/g, "|")
|
||||
.split("|")
|
||||
.forEach((keyword) => keywordsSet.add(keyword));
|
||||
}
|
||||
}
|
||||
|
||||
const stringValue = Array.from(keywordsSet).join(",");
|
||||
this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue });
|
||||
}
|
||||
|
||||
|
||||
@@ -38,14 +38,24 @@ describe("generateRandomCustomElementName", () => {
|
||||
|
||||
describe("sendExtensionMessage", () => {
|
||||
it("sends a message to the extension", async () => {
|
||||
chrome.runtime.sendMessage = jest.fn().mockResolvedValue("sendMessageResponse");
|
||||
const extensionMessagePromise = sendExtensionMessage("some-extension-message");
|
||||
|
||||
const response = await sendExtensionMessage("some-extension-message", { value: "value" });
|
||||
// Jest doesn't give anyway to select the typed overload of "sendMessage",
|
||||
// a cast is needed to get the correct spy type.
|
||||
const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage") as unknown as jest.SpyInstance<
|
||||
void,
|
||||
[message: string, responseCallback: (response: string) => void],
|
||||
unknown
|
||||
>;
|
||||
|
||||
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
command: "some-extension-message",
|
||||
value: "value",
|
||||
});
|
||||
expect(sendMessageSpy).toHaveBeenCalled();
|
||||
|
||||
const [latestCall] = sendMessageSpy.mock.calls;
|
||||
const responseCallback = latestCall[1];
|
||||
|
||||
responseCallback("sendMessageResponse");
|
||||
|
||||
const response = await extensionMessagePromise;
|
||||
expect(response).toEqual("sendMessageResponse");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +105,19 @@ export async function sendExtensionMessage(
|
||||
command: string,
|
||||
options: Record<string, any> = {},
|
||||
): Promise<any> {
|
||||
return chrome.runtime.sendMessage({ command, ...options });
|
||||
if (typeof browser !== "undefined") {
|
||||
return browser.runtime.sendMessage({ command, ...options });
|
||||
}
|
||||
|
||||
return new Promise((resolve) =>
|
||||
chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -452,6 +452,9 @@ export default class MainBackground {
|
||||
return new ForegroundMemoryStorageService();
|
||||
}
|
||||
|
||||
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
|
||||
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
|
||||
// so that MAC failures are not logged.
|
||||
return new LocalBackedSessionStorageService(
|
||||
sessionKey,
|
||||
this.storageService,
|
||||
@@ -849,6 +852,7 @@ export default class MainBackground {
|
||||
this.sendService,
|
||||
this.sendApiService,
|
||||
messageListener,
|
||||
this.stateProvider,
|
||||
);
|
||||
} else {
|
||||
this.syncService = new DefaultSyncService(
|
||||
@@ -876,6 +880,7 @@ export default class MainBackground {
|
||||
this.billingAccountProfileStateService,
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.syncServiceListener = new SyncServiceListener(
|
||||
@@ -1047,6 +1052,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.authService,
|
||||
this.biometricStateService,
|
||||
this.accountService,
|
||||
);
|
||||
this.commandsBackground = new CommandsBackground(
|
||||
this,
|
||||
@@ -1358,7 +1364,6 @@ export default class MainBackground {
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.syncService.setLastSync(new Date(0), userBeingLoggedOut),
|
||||
this.cryptoService.clearKeys(userBeingLoggedOut),
|
||||
this.cipherService.clear(userBeingLoggedOut),
|
||||
this.folderService.clear(userBeingLoggedOut),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
@@ -81,6 +82,7 @@ export class NativeMessagingBackground {
|
||||
private logService: LogService,
|
||||
private authService: AuthService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
if (chrome?.permissions?.onAdded) {
|
||||
// Reload extension to activate nativeMessaging
|
||||
@@ -223,6 +225,16 @@ export class NativeMessagingBackground {
|
||||
});
|
||||
}
|
||||
|
||||
showIncorrectUserKeyDialog() {
|
||||
this.messagingService.send("showDialog", {
|
||||
title: { key: "nativeMessagingWrongUserKeyTitle" },
|
||||
content: { key: "nativeMessagingWrongUserKeyDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
|
||||
async send(message: Message) {
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
@@ -350,7 +362,26 @@ export class NativeMessagingBackground {
|
||||
const userKey = new SymmetricCryptoKey(
|
||||
Utils.fromB64ToArray(message.userKeyB64),
|
||||
) as UserKey;
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const isUserKeyValid = await this.cryptoService.validateUserKey(
|
||||
userKey,
|
||||
activeUserId,
|
||||
);
|
||||
if (isUserKeyValid) {
|
||||
await this.cryptoService.setUserKey(userKey, activeUserId);
|
||||
} else {
|
||||
this.logService.error("Unable to verify biometric unlocked userkey");
|
||||
await this.cryptoService.clearKeys(activeUserId);
|
||||
this.showIncorrectUserKeyDialog();
|
||||
|
||||
// Exit early
|
||||
if (this.resolver) {
|
||||
this.resolver(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
throw new Error("No key received");
|
||||
}
|
||||
@@ -371,21 +402,6 @@ export class NativeMessagingBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify key is correct by attempting to decrypt a secret
|
||||
try {
|
||||
await this.cryptoService.getFingerprint(await this.stateService.getUserId());
|
||||
} catch (e) {
|
||||
this.logService.error("Unable to verify key: " + e);
|
||||
await this.cryptoService.clearKeys();
|
||||
this.showWrongUserDialog();
|
||||
|
||||
// Exit early
|
||||
if (this.resolver) {
|
||||
this.resolver(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.runtimeBackground.processMessage({ command: "unlocked" });
|
||||
|
||||
@@ -278,12 +278,24 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
|
||||
async supportsBiometric() {
|
||||
const platformInfo = await BrowserApi.getPlatformInfo();
|
||||
if (platformInfo.os === "mac" || platformInfo.os === "win") {
|
||||
if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async biometricsNeedsSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async biometricsSupportsAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async biometricsSetup(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
authenticateBiometric() {
|
||||
return this.biometricCallback();
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
@@ -18,6 +21,7 @@ import { DO_FULL_SYNC, ForegroundSyncService, FullSyncMessage } from "./foregrou
|
||||
import { FullSyncFinishedMessage } from "./sync-service.listener";
|
||||
|
||||
describe("ForegroundSyncService", () => {
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const stateService = mock<StateService>();
|
||||
const folderService = mock<InternalFolderService>();
|
||||
const folderApiService = mock<FolderApiServiceAbstraction>();
|
||||
@@ -31,6 +35,7 @@ describe("ForegroundSyncService", () => {
|
||||
const sendService = mock<InternalSendService>();
|
||||
const sendApiService = mock<SendApiService>();
|
||||
const messageListener = mock<MessageListener>();
|
||||
const stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
|
||||
const sut = new ForegroundSyncService(
|
||||
stateService,
|
||||
@@ -46,6 +51,7 @@ describe("ForegroundSyncService", () => {
|
||||
sendService,
|
||||
sendApiService,
|
||||
messageListener,
|
||||
stateProvider,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MessageSender,
|
||||
} from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CoreSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
@@ -40,6 +41,7 @@ export class ForegroundSyncService extends CoreSyncService {
|
||||
sendService: InternalSendService,
|
||||
sendApiService: SendApiService,
|
||||
private readonly messageListener: MessageListener,
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(
|
||||
stateService,
|
||||
@@ -54,6 +56,7 @@ export class ForegroundSyncService extends CoreSyncService {
|
||||
authService,
|
||||
sendService,
|
||||
sendApiService,
|
||||
stateProvider,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"
|
||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
|
||||
@@ -63,7 +64,6 @@ import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-
|
||||
import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component";
|
||||
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
|
||||
import { SettingsComponent } from "../tools/popup/settings/settings.component";
|
||||
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
|
||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
|
||||
|
||||
@@ -35,6 +35,9 @@ import { SsoComponent } from "../auth/popup/sso.component";
|
||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
|
||||
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
|
||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
|
||||
@@ -58,9 +61,6 @@ import { SendTypeComponent } from "../tools/popup/send/send-type.component";
|
||||
import { SettingsComponent } from "../tools/popup/settings/settings.component";
|
||||
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
|
||||
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
|
||||
import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component";
|
||||
import { Fido2UseBrowserLinkComponent } from "../vault/popup/components/fido2/fido2-use-browser-link.component";
|
||||
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CLIENT_TYPE,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
@@ -82,6 +83,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||
import AutofillService from "../../autofill/services/autofill.service";
|
||||
import MainBackground from "../../background/main.background";
|
||||
@@ -521,6 +523,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useFactory: getBgService<ForegroundTaskSchedulerService>("taskSchedulerService"),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AnonLayoutWrapperDataService,
|
||||
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -8,12 +8,32 @@
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div *ngIf="sends.length === 0" class="tw-flex tw-flex-col tw-h-full tw-justify-center">
|
||||
<div
|
||||
*ngIf="listState === sendState.Empty"
|
||||
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
||||
>
|
||||
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
||||
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
|
||||
<tools-new-send-dropdown slot="button"></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
<app-send-list-items-container [sends]="sends" />
|
||||
|
||||
<ng-container *ngIf="listState !== sendState.Empty">
|
||||
<div
|
||||
*ngIf="listState === sendState.NoResults"
|
||||
class="tw-flex tw-flex-col tw-justify-center tw-h-auto tw-pt-12"
|
||||
>
|
||||
<bit-no-items [icon]="noResultsIcon">
|
||||
<ng-container slot="title">{{ "noItemsMatchSearch" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "clearFiltersOrTryAnother" | i18n }}</ng-container>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
<app-send-list-items-container [headerText]="title | i18n" [sends]="sends$ | async" />
|
||||
</ng-container>
|
||||
|
||||
<div slot="above-scroll-area" class="tw-p-4" *ngIf="listState !== sendState.Empty">
|
||||
<tools-send-search></tools-send-search>
|
||||
<app-send-list-filters></app-send-list-filters>
|
||||
</div>
|
||||
</popup-page>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of, BehaviorSubject } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
@@ -15,6 +16,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
@@ -22,7 +24,10 @@ import { ButtonModule, NoItemsModule } from "@bitwarden/components";
|
||||
import {
|
||||
NewSendDropdownComponent,
|
||||
SendListItemsContainerComponent,
|
||||
SendItemsService,
|
||||
SendSearchComponent,
|
||||
SendListFiltersComponent,
|
||||
SendListFiltersService,
|
||||
} from "@bitwarden/send-ui";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
@@ -30,31 +35,49 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
import { SendV2Component } from "./send-v2.component";
|
||||
import { SendV2Component, SendState } from "./send-v2.component";
|
||||
|
||||
describe("SendV2Component", () => {
|
||||
let component: SendV2Component;
|
||||
let fixture: ComponentFixture<SendV2Component>;
|
||||
let sendViews$: Observable<SendView[]>;
|
||||
let sendItemsService: MockProxy<SendItemsService>;
|
||||
let sendListFiltersService: SendListFiltersService;
|
||||
let sendListFiltersServiceFilters$: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let sendItemsServiceEmptyList$: BehaviorSubject<boolean>;
|
||||
let sendItemsServiceNoFilteredResults$: BehaviorSubject<boolean>;
|
||||
|
||||
beforeEach(async () => {
|
||||
sendViews$ = of([
|
||||
{ id: "1", name: "Send A" },
|
||||
{ id: "2", name: "Send B" },
|
||||
] as SendView[]);
|
||||
sendListFiltersServiceFilters$ = new BehaviorSubject({ sendType: null });
|
||||
sendItemsServiceEmptyList$ = new BehaviorSubject(false);
|
||||
sendItemsServiceNoFilteredResults$ = new BehaviorSubject(false);
|
||||
|
||||
sendItemsService = mock<SendItemsService>({
|
||||
filteredAndSortedSends$: of([
|
||||
{ id: "1", name: "Send A" },
|
||||
{ id: "2", name: "Send B" },
|
||||
] as SendView[]),
|
||||
latestSearchText$: of(""),
|
||||
});
|
||||
|
||||
sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder());
|
||||
|
||||
sendListFiltersService.filters$ = sendListFiltersServiceFilters$;
|
||||
sendItemsService.emptyList$ = sendItemsServiceEmptyList$;
|
||||
sendItemsService.noFilteredResults$ = sendItemsServiceNoFilteredResults$;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterTestingModule,
|
||||
JslibModule,
|
||||
NoItemsModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonModule,
|
||||
NoItemsModule,
|
||||
RouterLink,
|
||||
NewSendDropdownComponent,
|
||||
SendListItemsContainerComponent,
|
||||
SendListFiltersComponent,
|
||||
SendSearchComponent,
|
||||
SendV2Component,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
@@ -66,21 +89,24 @@ describe("SendV2Component", () => {
|
||||
{ provide: AvatarService, useValue: mock<AvatarService>() },
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: mock<BillingAccountProfileStateService>(),
|
||||
useValue: { hasPremiumFromAnySource$: of(false) },
|
||||
},
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: SendApiService, useValue: mock<SendApiService>() },
|
||||
{ provide: SendService, useValue: { sendViews$ } },
|
||||
{ provide: SendItemsService, useValue: mock<SendItemsService>() },
|
||||
{ provide: SearchService, useValue: mock<SearchService>() },
|
||||
{ provide: SendService, useValue: { sendViews$: new BehaviorSubject<SendView[]>([]) } },
|
||||
{ provide: SendItemsService, useValue: sendItemsService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SendV2Component);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -88,14 +114,21 @@ describe("SendV2Component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should sort sends by name on initialization", async () => {
|
||||
const sortedSends = [
|
||||
{ id: "1", name: "Send A" },
|
||||
{ id: "2", name: "Send B" },
|
||||
] as SendView[];
|
||||
it("should update the title based on the current filter", () => {
|
||||
sendListFiltersServiceFilters$.next({ sendType: SendType.File });
|
||||
fixture.detectChanges();
|
||||
expect(component["title"]).toBe("fileSends");
|
||||
});
|
||||
|
||||
await component.ngOnInit();
|
||||
it("should set listState to Empty when emptyList$ emits true", () => {
|
||||
sendItemsServiceEmptyList$.next(true);
|
||||
fixture.detectChanges();
|
||||
expect(component["listState"]).toBe(SendState.Empty);
|
||||
});
|
||||
|
||||
expect(component.sends).toEqual(sortedSends);
|
||||
it("should set listState to NoResults when noFilteredResults$ emits true", () => {
|
||||
sendItemsServiceNoFilteredResults$.next(true);
|
||||
fixture.detectChanges();
|
||||
expect(component["listState"]).toBe(SendState.NoResults);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { mergeMap, Subject, takeUntil } from "rxjs";
|
||||
import { combineLatest } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { ButtonModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import {
|
||||
NoSendsIcon,
|
||||
NewSendDropdownComponent,
|
||||
SendListItemsContainerComponent,
|
||||
SendItemsService,
|
||||
SendSearchComponent,
|
||||
SendListFiltersComponent,
|
||||
SendListFiltersService,
|
||||
} from "@bitwarden/send-ui";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
@@ -20,6 +22,11 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
export enum SendState {
|
||||
Empty,
|
||||
NoResults,
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "send-v2.component.html",
|
||||
standalone: true,
|
||||
@@ -36,29 +43,56 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
NewSendDropdownComponent,
|
||||
SendListItemsContainerComponent,
|
||||
SendListFiltersComponent,
|
||||
SendSearchComponent,
|
||||
],
|
||||
})
|
||||
export class SendV2Component implements OnInit, OnDestroy {
|
||||
sendType = SendType;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
sendState = SendState;
|
||||
|
||||
sends: SendView[] = [];
|
||||
protected listState: SendState | null = null;
|
||||
|
||||
protected sends$ = this.sendItemsService.filteredAndSortedSends$;
|
||||
|
||||
protected title: string = "allSends";
|
||||
|
||||
protected noItemIcon = NoSendsIcon;
|
||||
|
||||
constructor(protected sendService: SendService) {}
|
||||
protected noResultsIcon = Icons.NoResults;
|
||||
|
||||
async ngOnInit() {
|
||||
this.sendService.sendViews$
|
||||
.pipe(
|
||||
mergeMap(async (sends) => {
|
||||
this.sends = sends.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
constructor(
|
||||
protected sendItemsService: SendItemsService,
|
||||
protected sendListFiltersService: SendListFiltersService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.sendItemsService.emptyList$,
|
||||
this.sendItemsService.noFilteredResults$,
|
||||
this.sendListFiltersService.filters$,
|
||||
])
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(([emptyList, noFilteredResults, currentFilter]) => {
|
||||
if (currentFilter?.sendType !== null) {
|
||||
this.title = `${this.sendType[currentFilter.sendType].toLowerCase()}Sends`;
|
||||
} else {
|
||||
this.title = "allSends";
|
||||
}
|
||||
|
||||
if (emptyList) {
|
||||
this.listState = SendState.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
if (noFilteredResults) {
|
||||
this.listState = SendState.NoResults;
|
||||
return;
|
||||
}
|
||||
|
||||
this.listState = null;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,22 @@ import { closeFido2Popout, openFido2Popout } from "../popup/utils/vault-popout-w
|
||||
|
||||
const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
|
||||
|
||||
export const BrowserFido2MessageTypes = {
|
||||
ConnectResponse: "ConnectResponse",
|
||||
NewSessionCreatedRequest: "NewSessionCreatedRequest",
|
||||
PickCredentialRequest: "PickCredentialRequest",
|
||||
PickCredentialResponse: "PickCredentialResponse",
|
||||
ConfirmNewCredentialRequest: "ConfirmNewCredentialRequest",
|
||||
ConfirmNewCredentialResponse: "ConfirmNewCredentialResponse",
|
||||
InformExcludedCredentialRequest: "InformExcludedCredentialRequest",
|
||||
InformCredentialNotFoundRequest: "InformCredentialNotFoundRequest",
|
||||
AbortRequest: "AbortRequest",
|
||||
AbortResponse: "AbortResponse",
|
||||
} as const;
|
||||
|
||||
export type BrowserFido2MessageTypeValue =
|
||||
(typeof BrowserFido2MessageTypes)[keyof typeof BrowserFido2MessageTypes];
|
||||
|
||||
export class SessionClosedError extends Error {
|
||||
constructor() {
|
||||
super("Fido2UserInterfaceSession was closed");
|
||||
@@ -39,30 +55,30 @@ export class SessionClosedError extends Error {
|
||||
export type BrowserFido2Message = { sessionId: string } & (
|
||||
| /**
|
||||
* This message is used by popouts to announce that they are ready
|
||||
* to recieve messages.
|
||||
* to receive messages.
|
||||
**/ {
|
||||
type: "ConnectResponse";
|
||||
type: typeof BrowserFido2MessageTypes.ConnectResponse;
|
||||
}
|
||||
/**
|
||||
* This message is used to announce the creation of a new session.
|
||||
* It is used by popouts to know when to close.
|
||||
**/
|
||||
| {
|
||||
type: "NewSessionCreatedRequest";
|
||||
type: typeof BrowserFido2MessageTypes.NewSessionCreatedRequest;
|
||||
}
|
||||
| {
|
||||
type: "PickCredentialRequest";
|
||||
type: typeof BrowserFido2MessageTypes.PickCredentialRequest;
|
||||
cipherIds: string[];
|
||||
userVerification: boolean;
|
||||
fallbackSupported: boolean;
|
||||
}
|
||||
| {
|
||||
type: "PickCredentialResponse";
|
||||
type: typeof BrowserFido2MessageTypes.PickCredentialResponse;
|
||||
cipherId?: string;
|
||||
userVerified: boolean;
|
||||
}
|
||||
| {
|
||||
type: "ConfirmNewCredentialRequest";
|
||||
type: typeof BrowserFido2MessageTypes.ConfirmNewCredentialRequest;
|
||||
credentialName: string;
|
||||
userName: string;
|
||||
userHandle: string;
|
||||
@@ -71,24 +87,24 @@ export type BrowserFido2Message = { sessionId: string } & (
|
||||
rpId: string;
|
||||
}
|
||||
| {
|
||||
type: "ConfirmNewCredentialResponse";
|
||||
type: typeof BrowserFido2MessageTypes.ConfirmNewCredentialResponse;
|
||||
cipherId: string;
|
||||
userVerified: boolean;
|
||||
}
|
||||
| {
|
||||
type: "InformExcludedCredentialRequest";
|
||||
type: typeof BrowserFido2MessageTypes.InformExcludedCredentialRequest;
|
||||
existingCipherIds: string[];
|
||||
fallbackSupported: boolean;
|
||||
}
|
||||
| {
|
||||
type: "InformCredentialNotFoundRequest";
|
||||
type: typeof BrowserFido2MessageTypes.InformCredentialNotFoundRequest;
|
||||
fallbackSupported: boolean;
|
||||
}
|
||||
| {
|
||||
type: "AbortRequest";
|
||||
type: typeof BrowserFido2MessageTypes.AbortRequest;
|
||||
}
|
||||
| {
|
||||
type: "AbortResponse";
|
||||
type: typeof BrowserFido2MessageTypes.AbortResponse;
|
||||
fallbackRequested: boolean;
|
||||
}
|
||||
);
|
||||
@@ -138,7 +154,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
static abortPopout(sessionId: string, fallbackRequested = false) {
|
||||
this.sendMessage({
|
||||
sessionId: sessionId,
|
||||
type: "AbortResponse",
|
||||
type: BrowserFido2MessageTypes.AbortResponse,
|
||||
fallbackRequested: fallbackRequested,
|
||||
});
|
||||
}
|
||||
@@ -146,7 +162,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
static confirmNewCredentialResponse(sessionId: string, cipherId: string, userVerified: boolean) {
|
||||
this.sendMessage({
|
||||
sessionId: sessionId,
|
||||
type: "ConfirmNewCredentialResponse",
|
||||
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
|
||||
cipherId,
|
||||
userVerified,
|
||||
});
|
||||
@@ -169,7 +185,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
) {
|
||||
this.messages$
|
||||
.pipe(
|
||||
filter((msg) => msg.type === "ConnectResponse"),
|
||||
filter((msg) => msg.type === BrowserFido2MessageTypes.ConnectResponse),
|
||||
take(1),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
@@ -185,7 +201,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.close();
|
||||
BrowserFido2UserInterfaceSession.sendMessage({
|
||||
type: "AbortRequest",
|
||||
type: BrowserFido2MessageTypes.AbortRequest,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
});
|
||||
@@ -193,12 +209,12 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
// Handle session aborted by user
|
||||
this.messages$
|
||||
.pipe(
|
||||
filter((msg) => msg.type === "AbortResponse"),
|
||||
filter((msg) => msg.type === BrowserFido2MessageTypes.AbortResponse),
|
||||
take(1),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((msg) => {
|
||||
if (msg.type === "AbortResponse") {
|
||||
if (msg.type === BrowserFido2MessageTypes.AbortResponse) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.close();
|
||||
@@ -217,7 +233,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
);
|
||||
|
||||
BrowserFido2UserInterfaceSession.sendMessage({
|
||||
type: "NewSessionCreatedRequest",
|
||||
type: BrowserFido2MessageTypes.NewSessionCreatedRequest,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
@@ -227,7 +243,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
userVerification,
|
||||
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
const data: BrowserFido2Message = {
|
||||
type: "PickCredentialRequest",
|
||||
type: BrowserFido2MessageTypes.PickCredentialRequest,
|
||||
cipherIds,
|
||||
sessionId: this.sessionId,
|
||||
userVerification,
|
||||
@@ -235,7 +251,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
};
|
||||
|
||||
await this.send(data);
|
||||
const response = await this.receive("PickCredentialResponse");
|
||||
const response = await this.receive(BrowserFido2MessageTypes.PickCredentialResponse);
|
||||
|
||||
return { cipherId: response.cipherId, userVerified: response.userVerified };
|
||||
}
|
||||
@@ -248,7 +264,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
rpId,
|
||||
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
const data: BrowserFido2Message = {
|
||||
type: "ConfirmNewCredentialRequest",
|
||||
type: BrowserFido2MessageTypes.ConfirmNewCredentialRequest,
|
||||
sessionId: this.sessionId,
|
||||
credentialName,
|
||||
userName,
|
||||
@@ -259,21 +275,21 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
};
|
||||
|
||||
await this.send(data);
|
||||
const response = await this.receive("ConfirmNewCredentialResponse");
|
||||
const response = await this.receive(BrowserFido2MessageTypes.ConfirmNewCredentialResponse);
|
||||
|
||||
return { cipherId: response.cipherId, userVerified: response.userVerified };
|
||||
}
|
||||
|
||||
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
|
||||
const data: BrowserFido2Message = {
|
||||
type: "InformExcludedCredentialRequest",
|
||||
type: BrowserFido2MessageTypes.InformExcludedCredentialRequest,
|
||||
sessionId: this.sessionId,
|
||||
existingCipherIds,
|
||||
fallbackSupported: this.fallbackSupported,
|
||||
};
|
||||
|
||||
await this.send(data);
|
||||
await this.receive("AbortResponse");
|
||||
await this.receive(BrowserFido2MessageTypes.AbortResponse);
|
||||
}
|
||||
|
||||
async ensureUnlockedVault(): Promise<void> {
|
||||
@@ -284,13 +300,13 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
|
||||
async informCredentialNotFound(): Promise<void> {
|
||||
const data: BrowserFido2Message = {
|
||||
type: "InformCredentialNotFoundRequest",
|
||||
type: BrowserFido2MessageTypes.InformCredentialNotFoundRequest,
|
||||
sessionId: this.sessionId,
|
||||
fallbackSupported: this.fallbackSupported,
|
||||
};
|
||||
|
||||
await this.send(data);
|
||||
await this.receive("AbortResponse");
|
||||
await this.receive(BrowserFido2MessageTypes.AbortResponse);
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
[pageTitle]="headerText"
|
||||
[backAction]="handleBackButton.bind(this)"
|
||||
showBackButton
|
||||
></popup-header>
|
||||
>
|
||||
<app-pop-out slot="end" />
|
||||
</popup-header>
|
||||
|
||||
<vault-cipher-form
|
||||
*ngIf="!loading"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
@@ -118,6 +119,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
PopupFooterComponent,
|
||||
CipherFormModule,
|
||||
AsyncActionsModule,
|
||||
PopOutComponent,
|
||||
],
|
||||
})
|
||||
export class AddEditV2Component implements OnInit {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="headerText" showBackButton> </popup-header>
|
||||
<popup-header slot="header" [pageTitle]="headerText" showBackButton>
|
||||
<app-pop-out slot="end" />
|
||||
</popup-header>
|
||||
|
||||
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
|
||||
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
|
||||
@@ -45,6 +46,7 @@ import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup
|
||||
IconButtonModule,
|
||||
CipherViewComponent,
|
||||
AsyncActionsModule,
|
||||
PopOutComponent,
|
||||
],
|
||||
})
|
||||
export class ViewV2Component {
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"papaparse": "5.4.1",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tldts": "6.1.34",
|
||||
"tldts": "6.1.38",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ListResponse } from "./models/response/list.response";
|
||||
import { MessageResponse } from "./models/response/message.response";
|
||||
import { StringResponse } from "./models/response/string.response";
|
||||
import { TemplateResponse } from "./models/response/template.response";
|
||||
import { ServiceContainer } from "./service-container";
|
||||
import { ServiceContainer } from "./service-container/service-container";
|
||||
import { CliUtils } from "./utils";
|
||||
|
||||
const writeLn = CliUtils.writeLn;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { program } from "commander";
|
||||
import { OssServeConfigurator } from "./oss-serve-configurator";
|
||||
import { registerOssPrograms } from "./register-oss-programs";
|
||||
import { ServeProgram } from "./serve.program";
|
||||
import { ServiceContainer } from "./service-container";
|
||||
import { ServiceContainer } from "./service-container/service-container";
|
||||
|
||||
async function main() {
|
||||
const serviceContainer = new ServiceContainer();
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as koaJson from "koa-json";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { OssServeConfigurator } from "../oss-serve-configurator";
|
||||
import { ServiceContainer } from "../service-container";
|
||||
import { ServiceContainer } from "../service-container/service-container";
|
||||
|
||||
export class ServeCommand {
|
||||
constructor(
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RestoreCommand } from "./commands/restore.command";
|
||||
import { StatusCommand } from "./commands/status.command";
|
||||
import { Response } from "./models/response";
|
||||
import { FileResponse } from "./models/response/file.response";
|
||||
import { ServiceContainer } from "./service-container";
|
||||
import { ServiceContainer } from "./service-container/service-container";
|
||||
import { GenerateCommand } from "./tools/generate.command";
|
||||
import {
|
||||
SendEditCommand,
|
||||
|
||||
@@ -139,6 +139,18 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
biometricsNeedsSetup(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
biometricsSupportsAutoSetup(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
biometricsSetup(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
supportsSecureStorage(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Program } from "./program";
|
||||
import { ServiceContainer } from "./service-container";
|
||||
import { ServiceContainer } from "./service-container/service-container";
|
||||
import { SendProgram } from "./tools/send/send.program";
|
||||
import { VaultProgram } from "./vault.program";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { program } from "commander";
|
||||
import { BaseProgram } from "./base-program";
|
||||
import { ServeCommand } from "./commands/serve.command";
|
||||
import { OssServeConfigurator } from "./oss-serve-configurator";
|
||||
import { ServiceContainer } from "./service-container";
|
||||
import { ServiceContainer } from "./service-container/service-container";
|
||||
import { CliUtils } from "./utils";
|
||||
|
||||
const writeLn = CliUtils.writeLn;
|
||||
|
||||
@@ -44,7 +44,10 @@ import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
} from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import {
|
||||
DefaultDomainSettingsService,
|
||||
DomainSettingsService,
|
||||
@@ -147,18 +150,18 @@ import {
|
||||
VaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service";
|
||||
import { ConsoleLogService } from "./platform/services/console-log.service";
|
||||
import { I18nService } from "./platform/services/i18n.service";
|
||||
import { LowdbStorageService } from "./platform/services/lowdb-storage.service";
|
||||
import { NodeApiService } from "./platform/services/node-api.service";
|
||||
import { NodeEnvSecureStorageService } from "./platform/services/node-env-secure-storage.service";
|
||||
import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service";
|
||||
import { ConsoleLogService } from "../platform/services/console-log.service";
|
||||
import { I18nService } from "../platform/services/i18n.service";
|
||||
import { LowdbStorageService } from "../platform/services/lowdb-storage.service";
|
||||
import { NodeApiService } from "../platform/services/node-api.service";
|
||||
import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service";
|
||||
|
||||
// Polyfills
|
||||
global.DOMParser = new jsdom.JSDOM().window.DOMParser;
|
||||
|
||||
// eslint-disable-next-line
|
||||
const packageJson = require("../package.json");
|
||||
const packageJson = require("../../package.json");
|
||||
|
||||
/**
|
||||
* Instantiates services and makes them available for dependency injection.
|
||||
@@ -254,13 +257,13 @@ export class ServiceContainer {
|
||||
} else if (process.env.BITWARDENCLI_APPDATA_DIR) {
|
||||
p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR);
|
||||
} else if (process.platform === "darwin") {
|
||||
p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI");
|
||||
p = path.join(process.env.HOME ?? "", "Library/Application Support/Bitwarden CLI");
|
||||
} else if (process.platform === "win32") {
|
||||
p = path.join(process.env.APPDATA, "Bitwarden CLI");
|
||||
p = path.join(process.env.APPDATA ?? "", "Bitwarden CLI");
|
||||
} else if (process.env.XDG_CONFIG_HOME) {
|
||||
p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI");
|
||||
} else {
|
||||
p = path.join(process.env.HOME, ".config/Bitwarden CLI");
|
||||
p = path.join(process.env.HOME ?? "", ".config/Bitwarden CLI");
|
||||
}
|
||||
|
||||
const logoutCallback = async () => await this.logout();
|
||||
@@ -452,8 +455,6 @@ export class ServiceContainer {
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService);
|
||||
|
||||
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||
|
||||
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
||||
@@ -524,6 +525,40 @@ export class ServiceContainer {
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.authRequestService = new AuthRequestService(
|
||||
this.appIdService,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
this.messagingService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
);
|
||||
|
||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||
|
||||
this.configService = new DefaultConfigService(
|
||||
this.configApiService,
|
||||
this.environmentService,
|
||||
this.logService,
|
||||
this.stateProvider,
|
||||
this.authService,
|
||||
);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
this.deviceTrustService = new DeviceTrustService(
|
||||
this.keyGenerationService,
|
||||
@@ -541,20 +576,6 @@ export class ServiceContainer {
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.authRequestService = new AuthRequestService(
|
||||
this.appIdService,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService);
|
||||
this.loginStrategyService = new LoginStrategyService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
@@ -583,23 +604,10 @@ export class ServiceContainer {
|
||||
this.taskSchedulerService,
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
this.messagingService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
);
|
||||
|
||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||
|
||||
this.configService = new DefaultConfigService(
|
||||
this.configApiService,
|
||||
this.environmentService,
|
||||
this.logService,
|
||||
// FIXME: CLI does not support autofill
|
||||
this.autofillSettingsService = new AutofillSettingsService(
|
||||
this.stateProvider,
|
||||
this.authService,
|
||||
this.policyService,
|
||||
);
|
||||
|
||||
this.cipherService = new CipherService(
|
||||
@@ -661,7 +669,7 @@ export class ServiceContainer {
|
||||
this.taskSchedulerService,
|
||||
this.logService,
|
||||
lockedCallback,
|
||||
null,
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
@@ -691,6 +699,7 @@ export class ServiceContainer {
|
||||
this.billingAccountProfileStateService,
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
||||
@@ -752,6 +761,8 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService);
|
||||
|
||||
this.providerApiService = new ProviderApiService(this.apiService);
|
||||
}
|
||||
|
||||
@@ -762,7 +773,6 @@ export class ServiceContainer {
|
||||
const userId = (await this.stateService.getUserId()) as UserId;
|
||||
await Promise.all([
|
||||
this.eventUploadService.uploadEvents(userId as UserId),
|
||||
this.syncService.setLastSync(new Date(0)),
|
||||
this.cryptoService.clearKeys(),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
@@ -774,7 +784,7 @@ export class ServiceContainer {
|
||||
await this.stateService.clean();
|
||||
await this.accountService.clean(userId);
|
||||
await this.accountService.switchAccount(null);
|
||||
process.env.BW_SESSION = null;
|
||||
process.env.BW_SESSION = undefined;
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -790,7 +800,7 @@ export class ServiceContainer {
|
||||
this.twoFactorService.init();
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount) {
|
||||
if (activeAccount?.id) {
|
||||
await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id);
|
||||
}
|
||||
|
||||
7
apps/cli/src/service-container/tsconfig.json
Normal file
7
apps/cli/src/service-container/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": true
|
||||
}
|
||||
}
|
||||
21
apps/desktop/desktop_native/Cargo.lock
generated
21
apps/desktop/desktop_native/Cargo.lock
generated
@@ -282,12 +282,6 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -496,6 +490,7 @@ dependencies = [
|
||||
"core-foundation",
|
||||
"gio",
|
||||
"keytar",
|
||||
"libc",
|
||||
"libsecret",
|
||||
"rand",
|
||||
"retry",
|
||||
@@ -509,6 +504,7 @@ dependencies = [
|
||||
"widestring",
|
||||
"windows",
|
||||
"zbus",
|
||||
"zbus_polkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2282,6 +2278,19 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_polkit"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00a29bfa927b29f91b7feb4e1990f2dd1b4604072f493dc2f074cf59e4e0ba90"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"static_assertions",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.1.2"
|
||||
|
||||
@@ -17,6 +17,7 @@ arboard = { version = "=3.4.0", default-features = false, features = [
|
||||
] }
|
||||
base64 = "=0.22.1"
|
||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||
libc = "0.2.155"
|
||||
rand = "=0.8.5"
|
||||
retry = "=2.0.0"
|
||||
scopeguard = "=1.2.0"
|
||||
@@ -51,3 +52,4 @@ security-framework-sys = "=2.11.0"
|
||||
gio = "=0.19.5"
|
||||
libsecret = "=0.5.0"
|
||||
zbus = "4.3.1"
|
||||
zbus_polkit = "4.0.0"
|
||||
|
||||
@@ -6,11 +6,11 @@ use crate::biometric::{KeyMaterial, OsDerivedKey};
|
||||
pub struct Biometric {}
|
||||
|
||||
impl super::BiometricTrait for Biometric {
|
||||
fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
async fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn available() -> Result<bool> {
|
||||
async fn available() -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
@@ -6,6 +7,10 @@ use anyhow::Result;
|
||||
mod biometric;
|
||||
|
||||
pub use biometric::Biometric;
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::crypto::{self, CipherString};
|
||||
|
||||
pub struct KeyMaterial {
|
||||
pub os_key_part_b64: String,
|
||||
@@ -18,8 +23,10 @@ pub struct OsDerivedKey {
|
||||
}
|
||||
|
||||
pub trait BiometricTrait {
|
||||
fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool>;
|
||||
fn available() -> Result<bool>;
|
||||
#[allow(async_fn_in_trait)]
|
||||
async fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool>;
|
||||
#[allow(async_fn_in_trait)]
|
||||
async fn available() -> Result<bool>;
|
||||
fn derive_key_material(secret: Option<&str>) -> Result<OsDerivedKey>;
|
||||
fn set_biometric_secret(
|
||||
service: &str,
|
||||
@@ -34,3 +41,40 @@ pub trait BiometricTrait {
|
||||
key_material: Option<KeyMaterial>,
|
||||
) -> Result<String>;
|
||||
}
|
||||
|
||||
|
||||
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
|
||||
let iv = base64_engine
|
||||
.decode(iv_b64)?
|
||||
.try_into()
|
||||
.map_err(|e: Vec<_>| anyhow!("Expected length {}, got {}", 16, e.len()))?;
|
||||
|
||||
let encrypted = crypto::encrypt_aes256(secret.as_bytes(), iv, key_material.derive_key()?)?;
|
||||
|
||||
Ok(encrypted.to_string())
|
||||
}
|
||||
|
||||
fn decrypt(secret: &CipherString, key_material: &KeyMaterial) -> Result<String> {
|
||||
if let CipherString::AesCbc256_B64 { iv, data } = secret {
|
||||
let decrypted = crypto::decrypt_aes256(&iv, &data, key_material.derive_key()?)?;
|
||||
|
||||
Ok(String::from_utf8(decrypted)?)
|
||||
} else {
|
||||
Err(anyhow!("Invalid cipher string"))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyMaterial {
|
||||
fn digest_material(&self) -> String {
|
||||
match self.client_key_part_b64.as_deref() {
|
||||
Some(client_key_part_b64) => {
|
||||
format!("{}|{}", self.os_key_part_b64, client_key_part_b64)
|
||||
}
|
||||
None => self.os_key_part_b64.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_key(&self) -> Result<GenericArray<u8, typenum::U32>> {
|
||||
Ok(Sha256::digest(self.digest_material()))
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,109 @@
|
||||
use anyhow::{bail, Result};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::biometric::{KeyMaterial, OsDerivedKey};
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use rand::RngCore;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::biometric::{KeyMaterial, OsDerivedKey, base64_engine};
|
||||
use zbus::Connection;
|
||||
use zbus_polkit::policykit1::*;
|
||||
|
||||
use super::{decrypt, encrypt};
|
||||
use anyhow::anyhow;
|
||||
use crate::crypto::CipherString;
|
||||
|
||||
/// The Unix implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
|
||||
impl super::BiometricTrait for Biometric {
|
||||
fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
async fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
let connection = Connection::system().await?;
|
||||
let proxy = AuthorityProxy::new(&connection).await?;
|
||||
let subject = Subject::new_for_owner(std::process::id(), None, None)?;
|
||||
let details = std::collections::HashMap::new();
|
||||
let result = proxy.check_authorization(
|
||||
&subject,
|
||||
"com.bitwarden.Bitwarden.unlock",
|
||||
&details,
|
||||
CheckAuthorizationFlags::AllowUserInteraction.into(),
|
||||
"",
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(result) => {
|
||||
return Ok(result.is_authorized);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("polkit biometric error: {:?}", e);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn available() -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
async fn available() -> Result<bool> {
|
||||
let connection = Connection::system().await?;
|
||||
let proxy = AuthorityProxy::new(&connection).await?;
|
||||
let res = proxy.enumerate_actions("en").await?;
|
||||
for action in res {
|
||||
if action.action_id == "com.bitwarden.Bitwarden.unlock" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
fn derive_key_material(_iv_str: Option<&str>) -> Result<OsDerivedKey> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
fn derive_key_material(challenge_str: Option<&str>) -> Result<OsDerivedKey> {
|
||||
let challenge: [u8; 16] = match challenge_str {
|
||||
Some(challenge_str) => base64_engine
|
||||
.decode(challenge_str)?
|
||||
.try_into()
|
||||
.map_err(|e: Vec<_>| anyhow!("Expect length {}, got {}", 16, e.len()))?,
|
||||
None => random_challenge(),
|
||||
};
|
||||
|
||||
fn get_biometric_secret(
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
_key_material: Option<KeyMaterial>,
|
||||
) -> Result<String> {
|
||||
bail!("platform not supported");
|
||||
// there is no windows hello like interactive bio protected secret at the moment on linux
|
||||
// so we use a a key derived from the iv. this key is not intended to add any security
|
||||
// but only a place-holder
|
||||
let key = Sha256::digest(challenge);
|
||||
let key_b64 = base64_engine.encode(&key);
|
||||
let iv_b64 = base64_engine.encode(&challenge);
|
||||
Ok(OsDerivedKey { key_b64, iv_b64 })
|
||||
}
|
||||
|
||||
fn set_biometric_secret(
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
_secret: &str,
|
||||
_key_material: Option<KeyMaterial>,
|
||||
_iv_b64: &str,
|
||||
service: &str,
|
||||
account: &str,
|
||||
secret: &str,
|
||||
key_material: Option<KeyMaterial>,
|
||||
iv_b64: &str,
|
||||
) -> Result<String> {
|
||||
bail!("platform not supported");
|
||||
let key_material = key_material.ok_or(anyhow!(
|
||||
"Key material is required for polkit protected keys"
|
||||
))?;
|
||||
|
||||
let encrypted_secret = encrypt(secret, &key_material, iv_b64)?;
|
||||
crate::password::set_password(service, account, &encrypted_secret)?;
|
||||
Ok(encrypted_secret)
|
||||
}
|
||||
|
||||
fn get_biometric_secret(
|
||||
service: &str,
|
||||
account: &str,
|
||||
key_material: Option<KeyMaterial>,
|
||||
) -> Result<String> {
|
||||
let key_material = key_material.ok_or(anyhow!(
|
||||
"Key material is required for polkit protected keys"
|
||||
))?;
|
||||
|
||||
let encrypted_secret = crate::password::get_password(service, account)?;
|
||||
let secret = CipherString::from_str(&encrypted_secret)?;
|
||||
return Ok(decrypt(&secret, &key_material)?);
|
||||
}
|
||||
}
|
||||
|
||||
fn random_challenge() -> [u8; 16] {
|
||||
let mut challenge = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut challenge);
|
||||
challenge
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
use rand::RngCore;
|
||||
@@ -30,14 +29,16 @@ use windows::{
|
||||
|
||||
use crate::{
|
||||
biometric::{KeyMaterial, OsDerivedKey},
|
||||
crypto::{self, CipherString},
|
||||
crypto::CipherString,
|
||||
};
|
||||
|
||||
use super::{decrypt, encrypt};
|
||||
|
||||
/// The Windows OS implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
|
||||
impl super::BiometricTrait for Biometric {
|
||||
fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
|
||||
async fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
|
||||
let h = isize::from_le_bytes(hwnd.clone().try_into().unwrap());
|
||||
let window = HWND(h);
|
||||
|
||||
@@ -56,7 +57,7 @@ impl super::BiometricTrait for Biometric {
|
||||
}
|
||||
}
|
||||
|
||||
fn available() -> Result<bool> {
|
||||
async fn available() -> Result<bool> {
|
||||
let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?;
|
||||
|
||||
match ucv_available {
|
||||
@@ -159,26 +160,6 @@ impl super::BiometricTrait for Biometric {
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
|
||||
let iv = base64_engine
|
||||
.decode(iv_b64)?
|
||||
.try_into()
|
||||
.map_err(|e: Vec<_>| anyhow!("Expected length {}, got {}", 16, e.len()))?;
|
||||
|
||||
let encrypted = crypto::encrypt_aes256(secret.as_bytes(), iv, key_material.derive_key()?)?;
|
||||
|
||||
Ok(encrypted.to_string())
|
||||
}
|
||||
|
||||
fn decrypt(secret: &CipherString, key_material: &KeyMaterial) -> Result<String> {
|
||||
if let CipherString::AesCbc256_B64 { iv, data } = secret {
|
||||
let decrypted = crypto::decrypt_aes256(&iv, &data, key_material.derive_key()?)?;
|
||||
|
||||
Ok(String::from_utf8(decrypted)?)
|
||||
} else {
|
||||
Err(anyhow!("Invalid cipher string"))
|
||||
}
|
||||
}
|
||||
|
||||
fn random_challenge() -> [u8; 16] {
|
||||
let mut challenge = [0u8; 16];
|
||||
@@ -237,26 +218,11 @@ fn set_focus(window: HWND) {
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyMaterial {
|
||||
fn digest_material(&self) -> String {
|
||||
match self.client_key_part_b64.as_deref() {
|
||||
Some(client_key_part_b64) => {
|
||||
format!("{}|{}", self.os_key_part_b64, client_key_part_b64)
|
||||
}
|
||||
None => self.os_key_part_b64.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_key(&self) -> Result<GenericArray<u8, typenum::U32>> {
|
||||
Ok(Sha256::digest(self.digest_material()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::biometric::BiometricTrait;
|
||||
use crate::biometric::{encrypt, BiometricTrait};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
|
||||
@@ -3,4 +3,5 @@ pub mod clipboard;
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod password;
|
||||
pub mod process_isolation;
|
||||
pub mod powermonitor;
|
||||
|
||||
@@ -22,6 +22,10 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn is_available() -> Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -40,6 +40,17 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn is_available() -> Result<bool> {
|
||||
let result = password_clear_sync(Some(&get_schema()), build_attributes("bitwardenSecretsAvailabilityTest", "test"), gio::Cancellable::NONE);
|
||||
match result {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => {
|
||||
println!("secret-service unavailable: {:?}", result);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_schema() -> Schema {
|
||||
let mut attributes = std::collections::HashMap::new();
|
||||
attributes.insert("service", libsecret::SchemaAttributeType::String);
|
||||
|
||||
@@ -122,6 +122,10 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_available() -> Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn target_name(service: &str, account: &str) -> String {
|
||||
format!("{}/{}", service, account)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use anyhow::Result;
|
||||
use libc::{c_int, self};
|
||||
#[cfg(target_env = "gnu")]
|
||||
use libc::c_uint;
|
||||
|
||||
// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes
|
||||
// https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20
|
||||
#[cfg(target_env = "musl")]
|
||||
const RLIMIT_CORE: c_int = 4;
|
||||
#[cfg(target_env = "gnu")]
|
||||
const RLIMIT_CORE: c_uint = 4;
|
||||
|
||||
// PR_SET_DUMPABLE makes it so no other running process (root or same user) can dump the memory of this process
|
||||
// or attach a debugger to it.
|
||||
// https://github.com/torvalds/linux/blob/a38297e3fb012ddfa7ce0321a7e5a8daeb1872b6/include/uapi/linux/prctl.h#L14
|
||||
const PR_SET_DUMPABLE: c_int = 4;
|
||||
|
||||
pub fn disable_coredumps() -> Result<()> {
|
||||
let rlimit = libc::rlimit {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 {
|
||||
let e = std::io::Error::last_os_error();
|
||||
return Err(anyhow::anyhow!("failed to disable core dumping, memory might be persisted to disk on crashes {}", e))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
let mut rlimit = libc::rlimit {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
if unsafe { libc::getrlimit(RLIMIT_CORE, &mut rlimit) } != 0 {
|
||||
let e = std::io::Error::last_os_error();
|
||||
return Err(anyhow::anyhow!("failed to get core dump limit {}", e))
|
||||
}
|
||||
|
||||
Ok(rlimit.rlim_cur == 0 && rlimit.rlim_max == 0)
|
||||
}
|
||||
|
||||
pub fn disable_memory_access() -> Result<()> {
|
||||
if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 {
|
||||
let e = std::io::Error::last_os_error();
|
||||
return Err(anyhow::anyhow!("failed to disable memory dumping, memory is dumpable by other processes {}", e))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub fn disable_coredumps() -> Result<()> {
|
||||
bail!("Not implemented on Mac")
|
||||
}
|
||||
|
||||
pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
bail!("Not implemented on Mac")
|
||||
}
|
||||
|
||||
pub fn disable_memory_access() -> Result<()> {
|
||||
bail!("Not implemented on Mac")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod process_isolation;
|
||||
pub use process_isolation::*;
|
||||
@@ -0,0 +1,13 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub fn disable_coredumps() -> Result<()> {
|
||||
bail!("Not implemented on Windows")
|
||||
}
|
||||
|
||||
pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
bail!("Not implemented on Windows")
|
||||
}
|
||||
|
||||
pub fn disable_memory_access() -> Result<()> {
|
||||
bail!("Not implemented on Windows")
|
||||
}
|
||||
7
apps/desktop/desktop_native/napi/index.d.ts
vendored
7
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -12,6 +12,7 @@ export namespace passwords {
|
||||
export function setPassword(service: string, account: string, password: string): Promise<void>
|
||||
/** Delete the stored password from the keychain. */
|
||||
export function deletePassword(service: string, account: string): Promise<void>
|
||||
export function isAvailable(): Promise<boolean>
|
||||
}
|
||||
export namespace biometrics {
|
||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||
@@ -41,6 +42,12 @@ export namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
export namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function disableMemoryAccess(): Promise<void>
|
||||
}
|
||||
|
||||
export namespace powermonitors {
|
||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
|
||||
@@ -206,9 +206,10 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { passwords, biometrics, clipboards, powermonitors } = nativeBinding
|
||||
const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding
|
||||
|
||||
module.exports.passwords = passwords
|
||||
module.exports.biometrics = biometrics
|
||||
module.exports.clipboards = clipboards
|
||||
module.exports.processisolations = processisolations
|
||||
module.exports.powermonitors = powermonitors
|
||||
|
||||
@@ -33,6 +33,12 @@ pub mod passwords {
|
||||
desktop_core::password::delete_password(&service, &account)
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
// Checks if the os secure storage is available
|
||||
#[napi]
|
||||
pub async fn is_available() -> napi::Result<bool> {
|
||||
desktop_core::password::is_available().map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -45,12 +51,12 @@ pub mod biometrics {
|
||||
hwnd: napi::bindgen_prelude::Buffer,
|
||||
message: String,
|
||||
) -> napi::Result<bool> {
|
||||
Biometric::prompt(hwnd.into(), message).map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
Biometric::prompt(hwnd.into(), message).await.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn available() -> napi::Result<bool> {
|
||||
Biometric::available().map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
Biometric::available().await.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -142,6 +148,25 @@ pub mod clipboards {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod processisolations {
|
||||
#[napi]
|
||||
pub async fn disable_coredumps() -> napi::Result<()> {
|
||||
desktop_core::process_isolation::disable_coredumps()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
#[napi]
|
||||
pub async fn is_core_dumping_disabled() -> napi::Result<bool> {
|
||||
desktop_core::process_isolation::is_core_dumping_disabled()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
#[napi]
|
||||
pub async fn disable_memory_access() -> napi::Result<()> {
|
||||
desktop_core::process_isolation::disable_memory_access()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod powermonitors {
|
||||
use napi::{threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode}, tokio};
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
# disable core dumps
|
||||
ulimit -c 0
|
||||
|
||||
APP_PATH=$(dirname "$0")
|
||||
# might be behind symlink
|
||||
RAW_PATH=$(readlink -f "$0")
|
||||
APP_PATH=$(dirname $RAW_PATH)
|
||||
|
||||
# pass through all args
|
||||
$APP_PATH/bitwarden-app "$@"
|
||||
$APP_PATH/bitwarden-app "$@"
|
||||
|
||||
|
||||
@@ -126,11 +126,14 @@
|
||||
{{ biometricText | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<small class="help-block" *ngIf="this.form.value.biometric">{{
|
||||
<small class="help-block" *ngIf="this.form.value.biometric && !this.isLinux">{{
|
||||
additionalBiometricSettingsText | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="supportsBiometric && this.form.value.biometric">
|
||||
<div
|
||||
class="form-group"
|
||||
*ngIf="supportsBiometric && this.form.value.biometric && !this.isLinux"
|
||||
>
|
||||
<div class="checkbox form-group-child">
|
||||
<label for="autoPromptBiometrics">
|
||||
<input
|
||||
@@ -148,7 +151,8 @@
|
||||
*ngIf="
|
||||
supportsBiometric &&
|
||||
this.form.value.biometric &&
|
||||
(userHasMasterPassword || (this.form.value.pin && userHasPinSet))
|
||||
(userHasMasterPassword || (this.form.value.pin && userHasPinSet)) &&
|
||||
!this.isLinux
|
||||
"
|
||||
>
|
||||
<div class="checkbox form-group-child">
|
||||
|
||||
@@ -55,6 +55,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
requireEnableTray = false;
|
||||
showDuckDuckGoIntegrationOption = false;
|
||||
isWindows: boolean;
|
||||
isLinux: boolean;
|
||||
|
||||
enableTrayText: string;
|
||||
enableTrayDescText: string;
|
||||
@@ -197,6 +198,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.isWindows = (await this.platformUtilsService.getDevice()) === DeviceType.WindowsDesktop;
|
||||
this.isLinux = (await this.platformUtilsService.getDevice()) === DeviceType.LinuxDesktop;
|
||||
|
||||
if ((await this.stateService.getUserId()) == null) {
|
||||
return;
|
||||
@@ -464,6 +466,26 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const needsSetup = await this.platformUtilsService.biometricsNeedsSetup();
|
||||
const supportsBiometricAutoSetup =
|
||||
await this.platformUtilsService.biometricsSupportsAutoSetup();
|
||||
|
||||
if (needsSetup) {
|
||||
if (supportsBiometricAutoSetup) {
|
||||
await this.platformUtilsService.biometricsSetup();
|
||||
} else {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "biometricsManualSetupTitle" },
|
||||
content: { key: "biometricsManualSetupDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
if (this.isWindows) {
|
||||
// Recommended settings for Windows Hello
|
||||
@@ -472,6 +494,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
await this.biometricStateService.setPromptAutomatically(false);
|
||||
await this.biometricStateService.setRequirePasswordOnStart(true);
|
||||
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
|
||||
} else if (this.isLinux) {
|
||||
// Similar to Windows
|
||||
this.form.controls.requirePasswordOnStart.setValue(true);
|
||||
this.form.controls.autoPromptBiometrics.setValue(false);
|
||||
await this.biometricStateService.setPromptAutomatically(false);
|
||||
await this.biometricStateService.setRequirePasswordOnStart(true);
|
||||
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
|
||||
}
|
||||
await this.cryptoService.refreshAdditionalKeys();
|
||||
|
||||
@@ -624,7 +653,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.form.controls.enableBrowserIntegration.setValue(false);
|
||||
return;
|
||||
} else if (ipc.platform.deviceType === DeviceType.LinuxDesktop) {
|
||||
} else if (ipc.platform.isSnapStore || ipc.platform.isFlatpak) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "browserIntegrationUnsupportedTitle" },
|
||||
content: { key: "browserIntegrationLinuxDesc" },
|
||||
@@ -735,6 +764,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
return "unlockWithTouchId";
|
||||
case DeviceType.WindowsDesktop:
|
||||
return "unlockWithWindowsHello";
|
||||
case DeviceType.LinuxDesktop:
|
||||
return "unlockWithPolkit";
|
||||
default:
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
@@ -746,6 +777,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
return "autoPromptTouchId";
|
||||
case DeviceType.WindowsDesktop:
|
||||
return "autoPromptWindowsHello";
|
||||
case DeviceType.LinuxDesktop:
|
||||
return "autoPromptPolkit";
|
||||
default:
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
|
||||
@@ -650,7 +650,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Provide the userId of the user to upload events for
|
||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
||||
await this.cipherService.clear(userBeingLoggedOut);
|
||||
await this.folderService.clear(userBeingLoggedOut);
|
||||
|
||||
@@ -217,6 +217,8 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro
|
||||
return "unlockWithTouchId";
|
||||
case DeviceType.WindowsDesktop:
|
||||
return "unlockWithWindowsHello";
|
||||
case DeviceType.LinuxDesktop:
|
||||
return "unlockWithPolkit";
|
||||
default:
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
|
||||
@@ -1510,9 +1510,15 @@
|
||||
"additionalWindowsHelloSettings": {
|
||||
"message": "Additional Windows Hello settings"
|
||||
},
|
||||
"unlockWithPolkit": {
|
||||
"message": "Unlock with system authentication"
|
||||
},
|
||||
"windowsHelloConsentMessage": {
|
||||
"message": "Verify for Bitwarden."
|
||||
},
|
||||
"polkitConsentMessage": {
|
||||
"message": "Authenticate to unlock Bitwarden."
|
||||
},
|
||||
"unlockWithTouchId": {
|
||||
"message": "Unlock with Touch ID"
|
||||
},
|
||||
@@ -1525,6 +1531,9 @@
|
||||
"autoPromptWindowsHello": {
|
||||
"message": "Ask for Windows Hello on app start"
|
||||
},
|
||||
"autoPromptPolkit": {
|
||||
"message": "Ask for system authentication on launch"
|
||||
},
|
||||
"autoPromptTouchId": {
|
||||
"message": "Ask for Touch ID on app start"
|
||||
},
|
||||
@@ -1804,6 +1813,12 @@
|
||||
"biometricsNotEnabledDesc": {
|
||||
"message": "Browser biometrics requires desktop biometrics to be set up in the settings first."
|
||||
},
|
||||
"biometricsManualSetupTitle": {
|
||||
"message": "Autometic setup not available"
|
||||
},
|
||||
"biometricsManualSetupDesc": {
|
||||
"message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?"
|
||||
},
|
||||
"personalOwnershipSubmitError": {
|
||||
"message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections."
|
||||
},
|
||||
@@ -3028,5 +3043,11 @@
|
||||
},
|
||||
"data": {
|
||||
"message": "Data"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "File Sends"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { firstValueFrom } from "rxjs";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { processisolations } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { WindowState } from "../platform/models/domain/window-state";
|
||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||
@@ -31,6 +32,7 @@ export class WindowMain {
|
||||
private windowStateChangeTimer: NodeJS.Timeout;
|
||||
private windowStates: { [key: string]: WindowState } = {};
|
||||
private enableAlwaysOnTop = false;
|
||||
private enableRendererProcessForceCrashReload = false;
|
||||
session: Electron.Session;
|
||||
|
||||
readonly defaultWidth = 950;
|
||||
@@ -53,9 +55,11 @@ export class WindowMain {
|
||||
this.win.setBackgroundColor(await this.getBackgroundColor());
|
||||
|
||||
// By default some linux distro collect core dumps on crashes which gets written to disk.
|
||||
const crashEvent = once(this.win.webContents, "render-process-gone");
|
||||
this.win.webContents.forcefullyCrashRenderer();
|
||||
await crashEvent;
|
||||
if (this.enableRendererProcessForceCrashReload) {
|
||||
const crashEvent = once(this.win.webContents, "render-process-gone");
|
||||
this.win.webContents.forcefullyCrashRenderer();
|
||||
await crashEvent;
|
||||
}
|
||||
|
||||
this.win.webContents.reloadIgnoringCache();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
@@ -101,6 +105,31 @@ export class WindowMain {
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
if (isMac() || isWindows()) {
|
||||
this.enableRendererProcessForceCrashReload = true;
|
||||
} else if (isLinux() && !isDev()) {
|
||||
if (await processisolations.isCoreDumpingDisabled()) {
|
||||
this.logService.info("Coredumps are disabled in renderer process");
|
||||
this.enableRendererProcessForceCrashReload = true;
|
||||
} else {
|
||||
this.logService.info("Disabling coredumps in main process");
|
||||
try {
|
||||
await processisolations.disableCoredumps();
|
||||
} catch (e) {
|
||||
this.logService.error("Failed to disable coredumps", e);
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.info(
|
||||
"Disabling external memory dumps & debugger access in main process",
|
||||
);
|
||||
try {
|
||||
await processisolations.disableMemoryAccess();
|
||||
} catch (e) {
|
||||
this.logService.error("Failed to disable memory access", e);
|
||||
}
|
||||
}
|
||||
|
||||
await this.createWindow();
|
||||
resolve();
|
||||
if (this.argvCallback != null) {
|
||||
|
||||
@@ -51,4 +51,14 @@ export default class BiometricDarwinMain implements OsBiometricService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,16 @@ export default class NoopBiometricsService implements OsBiometricService {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
|
||||
160
apps/desktop/src/platform/main/biometric/biometric.unix.main.ts
Normal file
160
apps/desktop/src/platform/main/biometric/biometric.unix.main.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
import { isFlatpak, isLinux, isSnapStore } from "../../../utils";
|
||||
|
||||
import { OsBiometricService } from "./biometrics.service.abstraction";
|
||||
|
||||
const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC
|
||||
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||
|
||||
<policyconfig>
|
||||
<action id="com.bitwarden.Bitwarden.unlock">
|
||||
<description>Unlock Bitwarden</description>
|
||||
<message>Authenticate to unlock Bitwarden</message>
|
||||
<defaults>
|
||||
<allow_any>no</allow_any>
|
||||
<allow_inactive>no</allow_inactive>
|
||||
<allow_active>auth_self</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
</policyconfig>`;
|
||||
const policyFileName = "com.bitwarden.Bitwarden.policy";
|
||||
const policyPath = "/usr/share/polkit-1/actions/";
|
||||
|
||||
export default class BiometricUnixMain implements OsBiometricService {
|
||||
constructor(
|
||||
private i18nservice: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
) {}
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
|
||||
async setBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
value: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<void> {
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
key,
|
||||
value,
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
}
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<string | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
|
||||
if (value == null || value == "") {
|
||||
return null;
|
||||
} else {
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storedValue = await biometrics.getBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
storageDetails.key_material,
|
||||
);
|
||||
return storedValue;
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
const hwnd = this.windowMain.win.getNativeWindowHandle();
|
||||
return await biometrics.prompt(hwnd, this.i18nservice.t("polkitConsentMessage"));
|
||||
}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
// We assume all linux distros have some polkit implementation
|
||||
// that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
|
||||
// Snap does not have access at the moment to polkit
|
||||
// This could be dynamically detected on dbus in the future.
|
||||
// We should check if a libsecret implementation is available on the system
|
||||
// because otherwise we cannot offlod the protected userkey to secure storage.
|
||||
return (await passwords.isAvailable()) && !isSnapStore();
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup(): Promise<boolean> {
|
||||
// check whether the polkit policy is loaded via dbus call to polkit
|
||||
return !(await biometrics.available());
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
// We cannot auto setup on snap or flatpak since the filesystem is sandboxed.
|
||||
// The user needs to manually set up the polkit policy outside of the sandbox
|
||||
// since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
|
||||
// the sandbox, once the policy is set up outside of the sandbox.
|
||||
return isLinux() && !isSnapStore() && !isFlatpak();
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {
|
||||
const process = spawn("pkexec", [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`,
|
||||
]);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
process.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject("Failed to set up polkit policy");
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey
|
||||
// when we want to force a re-derive of the key material.
|
||||
private setIv(iv: string) {
|
||||
this._iv = iv;
|
||||
this._osKeyHalf = null;
|
||||
}
|
||||
|
||||
private async getStorageDetails({
|
||||
clientKeyHalfB64,
|
||||
}: {
|
||||
clientKeyHalfB64: string;
|
||||
}): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> {
|
||||
if (this._osKeyHalf == null) {
|
||||
const keyMaterial = await biometrics.deriveKeyMaterial(this._iv);
|
||||
// osKeyHalf is based on the iv and in contrast to windows is not locked behind user verefication!
|
||||
this._osKeyHalf = keyMaterial.keyB64;
|
||||
this._iv = keyMaterial.ivB64;
|
||||
}
|
||||
|
||||
return {
|
||||
key_material: {
|
||||
osKeyPartB64: this._osKeyHalf,
|
||||
clientKeyPartB64: clientKeyHalfB64,
|
||||
},
|
||||
ivB64: this._iv,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -214,4 +214,14 @@ export default class BiometricWindowsMain implements OsBiometricService {
|
||||
clientKeyPartB64,
|
||||
};
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export abstract class BiometricsServiceAbstraction {
|
||||
abstract osSupportsBiometric(): Promise<boolean>;
|
||||
abstract osBiometricsNeedsSetup: () => Promise<boolean>;
|
||||
abstract osBiometricsCanAutoSetup: () => Promise<boolean>;
|
||||
abstract osBiometricsSetup: () => Promise<void>;
|
||||
abstract canAuthBiometric({
|
||||
service,
|
||||
key,
|
||||
@@ -26,6 +29,22 @@ export abstract class BiometricsServiceAbstraction {
|
||||
|
||||
export interface OsBiometricService {
|
||||
osSupportsBiometric(): Promise<boolean>;
|
||||
/**
|
||||
* Check whether support for biometric unlock requires setup. This can be automatic or manual.
|
||||
*
|
||||
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
|
||||
*/
|
||||
osBiometricsNeedsSetup: () => Promise<boolean>;
|
||||
/**
|
||||
* Check whether biometrics can be automatically setup, or requires user interaction.
|
||||
*
|
||||
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
|
||||
*/
|
||||
osBiometricsCanAutoSetup: () => Promise<boolean>;
|
||||
/**
|
||||
* Starts automatic biometric setup, which places the required configuration files / changes the required settings.
|
||||
*/
|
||||
osBiometricsSetup: () => Promise<void>;
|
||||
authenticateBiometric(): Promise<boolean>;
|
||||
getBiometricKey(
|
||||
service: string,
|
||||
|
||||
@@ -28,6 +28,8 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
||||
this.loadWindowsHelloService();
|
||||
} else if (platform === "darwin") {
|
||||
this.loadMacOSService();
|
||||
} else if (platform === "linux") {
|
||||
this.loadUnixService();
|
||||
} else {
|
||||
this.loadNoopBiometricsService();
|
||||
}
|
||||
@@ -49,6 +51,12 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
||||
this.platformSpecificService = new BiometricDarwinMain(this.i18nService);
|
||||
}
|
||||
|
||||
private loadUnixService() {
|
||||
// eslint-disable-next-line
|
||||
const BiometricUnixMain = require("./biometric.unix.main").default;
|
||||
this.platformSpecificService = new BiometricUnixMain(this.i18nService, this.windowMain);
|
||||
}
|
||||
|
||||
private loadNoopBiometricsService() {
|
||||
// eslint-disable-next-line
|
||||
const NoopBiometricsService = require("./biometric.noop.main").default;
|
||||
@@ -59,6 +67,18 @@ export class BiometricsService implements BiometricsServiceAbstraction {
|
||||
return await this.platformSpecificService.osSupportsBiometric();
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
return await this.platformSpecificService.osBiometricsNeedsSetup();
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup() {
|
||||
return await this.platformSpecificService.osBiometricsCanAutoSetup();
|
||||
}
|
||||
|
||||
async osBiometricsSetup() {
|
||||
await this.platformSpecificService.osBiometricsSetup();
|
||||
}
|
||||
|
||||
async canAuthBiometric({
|
||||
service,
|
||||
key,
|
||||
|
||||
@@ -79,6 +79,15 @@ export class DesktopCredentialStorageListener {
|
||||
case BiometricAction.OsSupported:
|
||||
val = await this.biometricService.osSupportsBiometric();
|
||||
break;
|
||||
case BiometricAction.NeedsSetup:
|
||||
val = await this.biometricService.osBiometricsNeedsSetup();
|
||||
break;
|
||||
case BiometricAction.Setup:
|
||||
await this.biometricService.osBiometricsSetup();
|
||||
break;
|
||||
case BiometricAction.CanAutoSetup:
|
||||
val = await this.biometricService.osBiometricsCanAutoSetup();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
UnencryptedMessageResponse,
|
||||
} from "../models/native-messaging";
|
||||
import { BiometricMessage, BiometricAction } from "../types/biometric-message";
|
||||
import { isDev, isMacAppStore, isWindowsStore } from "../utils";
|
||||
import { isDev, isFlatpak, isMacAppStore, isSnapStore, isWindowsStore } from "../utils";
|
||||
|
||||
import { ClipboardWriteMessage } from "./types/clipboard";
|
||||
|
||||
@@ -48,6 +48,18 @@ const biometric = {
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.OsSupported,
|
||||
} satisfies BiometricMessage),
|
||||
biometricsNeedsSetup: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.NeedsSetup,
|
||||
} satisfies BiometricMessage),
|
||||
biometricsSetup: (): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.Setup,
|
||||
} satisfies BiometricMessage),
|
||||
biometricsCanAutoSetup: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.CanAutoSetup,
|
||||
} satisfies BiometricMessage),
|
||||
authenticate: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.Authenticate,
|
||||
@@ -115,6 +127,8 @@ export default {
|
||||
isDev: isDev(),
|
||||
isMacAppStore: isMacAppStore(),
|
||||
isWindowsStore: isWindowsStore(),
|
||||
isFlatpak: isFlatpak(),
|
||||
isSnapStore: isSnapStore(),
|
||||
reloadProcess: () => ipcRenderer.send("reload-process"),
|
||||
log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
|
||||
ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),
|
||||
|
||||
@@ -135,6 +135,18 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
return await ipc.platform.biometric.osSupported();
|
||||
}
|
||||
|
||||
async biometricsNeedsSetup(): Promise<boolean> {
|
||||
return await ipc.platform.biometric.biometricsNeedsSetup();
|
||||
}
|
||||
|
||||
async biometricsSupportsAutoSetup(): Promise<boolean> {
|
||||
return await ipc.platform.biometric.biometricsCanAutoSetup();
|
||||
}
|
||||
|
||||
async biometricsSetup(): Promise<void> {
|
||||
return await ipc.platform.biometric.biometricsSetup();
|
||||
}
|
||||
|
||||
/** This method is used to authenticate the user presence _only_.
|
||||
* It should not be used in the process to retrieve
|
||||
* biometric keys, which has a separate authentication mechanism.
|
||||
|
||||
@@ -2,6 +2,9 @@ export enum BiometricAction {
|
||||
EnabledForUser = "enabled",
|
||||
OsSupported = "osSupported",
|
||||
Authenticate = "authenticate",
|
||||
NeedsSetup = "needsSetup",
|
||||
Setup = "setup",
|
||||
CanAutoSetup = "canAutoSetup",
|
||||
}
|
||||
|
||||
export type BiometricMessage = {
|
||||
|
||||
@@ -62,6 +62,10 @@ export function isWindowsStore() {
|
||||
return windows && windowsStore === true;
|
||||
}
|
||||
|
||||
export function isFlatpak() {
|
||||
return process.platform === "linux" && process.env.container != null;
|
||||
}
|
||||
|
||||
export function isWindowsPortable() {
|
||||
return isWindows() && process.env.PORTABLE_EXECUTABLE_DIR != null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.8.0",
|
||||
"version": "2024.7.3",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -6,59 +6,70 @@
|
||||
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6 tw-mb-0">
|
||||
<bit-label>{{ "defaultType" | i18n }}</bit-label>
|
||||
<bit-select formControlName="defaultType" id="defaultType">
|
||||
<bit-option *ngFor="let o of defaultTypes" [value]="o.value" [label]="o.name"></bit-option>
|
||||
<bit-label>{{ "overridePasswordTypePolicy" | i18n }}</bit-label>
|
||||
<bit-select formControlName="overridePasswordType" id="overrideType">
|
||||
<bit-option
|
||||
*ngFor="let o of overridePasswordTypeOptions"
|
||||
[value]="o.value"
|
||||
[label]="o.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "password" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minLength" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="5" max="128" formControlName="minLength" />
|
||||
</bit-form-field>
|
||||
<!-- password-specific policies -->
|
||||
<div *ngIf="showPasswordPolicies$ | async">
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "password" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minLength" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="5" max="128" formControlName="minLength" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minNumbers" />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minSpecial" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useUpper" id="useUpper" />
|
||||
<bit-label>A-Z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useLower" id="useLower" />
|
||||
<bit-label>a-z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useNumbers" id="useNumbers" />
|
||||
<bit-label>0-9</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minNumbers" />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minSpecial" />
|
||||
</bit-form-field>
|
||||
|
||||
<!-- passphrase-specific policies -->
|
||||
<div *ngIf="showPassphrasePolicies$ | async">
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minimumNumberOfWords" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="3" max="20" formControlName="minNumberWords" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="capitalize" id="capitalize" />
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="includeNumber" id="includeNumber" />
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useUpper" id="useUpper" />
|
||||
<bit-label>A-Z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useLower" id="useLower" />
|
||||
<bit-label>a-z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useNumbers" id="useNumbers" />
|
||||
<bit-label>0-9</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minimumNumberOfWords" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="3" max="20" formControlName="minNumberWords" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="capitalize" id="capitalize" />
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="includeNumber" id="includeNumber" />
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DefaultPassphraseBoundaries, DefaultPasswordBoundaries } from "@bitwarden/generator-core";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
@@ -19,20 +22,59 @@ export class PasswordGeneratorPolicy extends BasePolicy {
|
||||
})
|
||||
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
||||
data = this.formBuilder.group({
|
||||
defaultType: [null],
|
||||
minLength: [null, [Validators.min(5), Validators.max(128)]],
|
||||
overridePasswordType: [null],
|
||||
minLength: [
|
||||
null,
|
||||
[
|
||||
Validators.min(DefaultPasswordBoundaries.length.min),
|
||||
Validators.max(DefaultPasswordBoundaries.length.max),
|
||||
],
|
||||
],
|
||||
useUpper: [null],
|
||||
useLower: [null],
|
||||
useNumbers: [null],
|
||||
useSpecial: [null],
|
||||
minNumbers: [null, [Validators.min(0), Validators.max(9)]],
|
||||
minSpecial: [null, [Validators.min(0), Validators.max(9)]],
|
||||
minNumberWords: [null, [Validators.min(3), Validators.max(20)]],
|
||||
minNumbers: [
|
||||
null,
|
||||
[
|
||||
Validators.min(DefaultPasswordBoundaries.minDigits.min),
|
||||
Validators.max(DefaultPasswordBoundaries.minDigits.max),
|
||||
],
|
||||
],
|
||||
minSpecial: [
|
||||
null,
|
||||
[
|
||||
Validators.min(DefaultPasswordBoundaries.minSpecialCharacters.min),
|
||||
Validators.max(DefaultPasswordBoundaries.minSpecialCharacters.max),
|
||||
],
|
||||
],
|
||||
minNumberWords: [
|
||||
null,
|
||||
[
|
||||
Validators.min(DefaultPassphraseBoundaries.numWords.min),
|
||||
Validators.max(DefaultPassphraseBoundaries.numWords.max),
|
||||
],
|
||||
],
|
||||
capitalize: [null],
|
||||
includeNumber: [null],
|
||||
});
|
||||
|
||||
defaultTypes: { name: string; value: string }[];
|
||||
overridePasswordTypeOptions: { name: string; value: string }[];
|
||||
|
||||
// These subjects cache visibility of the sub-options for passwords
|
||||
// and passphrases; without them policy controls don't show up at all.
|
||||
private showPasswordPolicies = new BehaviorSubject<boolean>(true);
|
||||
private showPassphrasePolicies = new BehaviorSubject<boolean>(true);
|
||||
|
||||
/** Emits `true` when the password policy options should be displayed */
|
||||
get showPasswordPolicies$() {
|
||||
return this.showPasswordPolicies.asObservable();
|
||||
}
|
||||
|
||||
/** Emits `true` when the passphrase policy options should be displayed */
|
||||
get showPassphrasePolicies$() {
|
||||
return this.showPassphrasePolicies.asObservable();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
@@ -40,10 +82,27 @@ export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
||||
) {
|
||||
super();
|
||||
|
||||
this.defaultTypes = [
|
||||
this.overridePasswordTypeOptions = [
|
||||
{ name: i18nService.t("userPreference"), value: null },
|
||||
{ name: i18nService.t("password"), value: "password" },
|
||||
{ name: i18nService.t("password"), value: PASSWORD_POLICY_VALUE },
|
||||
{ name: i18nService.t("passphrase"), value: "passphrase" },
|
||||
];
|
||||
|
||||
this.data.valueChanges
|
||||
.pipe(isEnabled(PASSWORD_POLICY_VALUE), takeUntilDestroyed())
|
||||
.subscribe(this.showPasswordPolicies);
|
||||
this.data.valueChanges
|
||||
.pipe(isEnabled(PASSPHRASE_POLICY_VALUE), takeUntilDestroyed())
|
||||
.subscribe(this.showPassphrasePolicies);
|
||||
}
|
||||
}
|
||||
|
||||
const PASSWORD_POLICY_VALUE = "password";
|
||||
const PASSPHRASE_POLICY_VALUE = "passphrase";
|
||||
|
||||
function isEnabled(enabledValue: string) {
|
||||
return map((d: { overridePasswordType: string }) => {
|
||||
const type = d?.overridePasswordType ?? enabledValue;
|
||||
return type === enabledValue;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -323,7 +323,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.syncService.setLastSync(new Date(0)),
|
||||
this.cryptoService.clearKeys(),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
|
||||
@@ -26,11 +26,12 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { flagEnabled } from "../../../utils/flags";
|
||||
import { RouterService, StateService } from "../../core";
|
||||
import { RouterService } from "../../core";
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (this.hasProvider) {
|
||||
this.formGroup.controls.businessOwned.setValue(true);
|
||||
this.formGroup.controls.clientOwnerEmail.addValidators(Validators.required);
|
||||
this.changedOwnedBusiness();
|
||||
this.provider = await this.providerApiService.getProvider(this.providerId);
|
||||
const providerDefaultPlan = this.passwordManagerPlans.find(
|
||||
|
||||
@@ -38,7 +38,6 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
@@ -71,7 +70,6 @@ import { EventService } from "./event.service";
|
||||
import { InitService } from "./init.service";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { RouterService } from "./router.service";
|
||||
import { StateService as WebStateService } from "./state";
|
||||
import { WebFileDownloadService } from "./web-file-download.service";
|
||||
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
||||
|
||||
@@ -135,11 +133,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ModalService,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
safeProvider(WebStateService),
|
||||
safeProvider({
|
||||
provide: StateService,
|
||||
useExisting: WebStateService,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: FileDownloadService,
|
||||
useClass: WebFileDownloadService,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./core.module";
|
||||
export * from "./event.service";
|
||||
export * from "./router.service";
|
||||
export * from "./state/state.service";
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./state.service";
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
|
||||
import {
|
||||
MEMORY_STORAGE,
|
||||
SECURE_STORAGE,
|
||||
STATE_FACTORY,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||
|
||||
@Injectable()
|
||||
export class StateService extends BaseStateService<GlobalState, Account> {
|
||||
constructor(
|
||||
storageService: AbstractStorageService,
|
||||
@Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService,
|
||||
@Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService,
|
||||
logService: LogService,
|
||||
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
||||
accountService: AccountService,
|
||||
environmentService: EnvironmentService,
|
||||
tokenService: TokenService,
|
||||
migrationRunner: MigrationRunner,
|
||||
) {
|
||||
super(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
memoryStorageService,
|
||||
logService,
|
||||
stateFactory,
|
||||
accountService,
|
||||
environmentService,
|
||||
tokenService,
|
||||
migrationRunner,
|
||||
);
|
||||
}
|
||||
|
||||
override async getLastSync(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
return await super.getLastSync(options);
|
||||
}
|
||||
|
||||
override async setLastSync(value: string, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
return await super.setLastSync(value, options);
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,12 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
biometricsNeedsSetup: () => Promise<boolean>;
|
||||
biometricsSupportsAutoSetup(): Promise<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
biometricsSetup: () => Promise<void>;
|
||||
|
||||
supportsSecureStorage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4145,8 +4145,9 @@
|
||||
"minimumNumberOfWords": {
|
||||
"message": "Minimum number of words"
|
||||
},
|
||||
"defaultType": {
|
||||
"message": "Default type"
|
||||
"overridePasswordTypePolicy": {
|
||||
"message": "Password Type",
|
||||
"description": "Name of the password generator policy that overrides the user's password/passphrase selection."
|
||||
},
|
||||
"userPreference": {
|
||||
"message": "User preference"
|
||||
@@ -8794,6 +8795,12 @@
|
||||
"purchasedSeatsRemoved": {
|
||||
"message": "purchased seats removed"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "File Sends"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
},
|
||||
"includesXMembers": {
|
||||
"message": "for $COUNT$ member",
|
||||
"placeholders": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
OrganizationAuthRequestService,
|
||||
OrganizationAuthRequestApiService,
|
||||
} from "@bitwarden/bit-common/admin-console/auth-requests";
|
||||
import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container";
|
||||
import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container/service-container";
|
||||
|
||||
/**
|
||||
* Instantiates services and makes them available for dependency injection.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user