1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 23:13:36 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Vicki League
2024-10-02 09:58:03 -04:00
86 changed files with 2364 additions and 162 deletions

View File

@@ -604,6 +604,15 @@
"yourVaultIsLocked": {
"message": "Your vault is locked. Verify your identity to continue."
},
"yourVaultIsLockedV2": {
"message": "Your vault is locked"
},
"yourAccountIsLocked": {
"message": "Your account is locked"
},
"or": {
"message": "or"
},
"unlock": {
"message": "Unlock"
},
@@ -1936,6 +1945,9 @@
"unlockWithBiometrics": {
"message": "Unlock with biometrics"
},
"unlockWithMasterPassword": {
"message": "Unlock with master password"
},
"awaitDesktop": {
"message": "Awaiting confirmation from desktop"
},
@@ -3623,6 +3635,9 @@
"typePasskey": {
"message": "Passkey"
},
"accessing": {
"message": "Accessing"
},
"passkeyNotCopied": {
"message": "Passkey will not be copied"
},

View File

@@ -59,7 +59,7 @@ export class CurrentAccountComponent {
}
async currentAccountClicked() {
if (this.route.snapshot.data.state.includes("account-switcher")) {
if (this.route.snapshot.data?.state?.includes("account-switcher")) {
this.location.back();
} else {
await this.router.navigate(["/account-switcher"]);

View File

@@ -17,6 +17,8 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
LockIcon,
LockV2Component,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationStartComponent,
@@ -181,6 +183,7 @@ const routes: Routes = [
path: "lock",
component: LockComponent,
canActivate: [lockGuard()],
canMatch: [extensionRefreshRedirect("/lockV2")],
data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties,
},
...twofactorRefactorSwap(
@@ -438,6 +441,28 @@ const routes: Routes = [
],
},
),
{
path: "",
component: ExtensionAnonLayoutWrapperComponent,
children: [
{
path: "lockV2",
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
data: {
pageIcon: LockIcon,
pageTitle: "yourVaultIsLockedV2",
showReadonlyHostname: true,
showAcctSwitcher: true,
} satisfies ExtensionAnonLayoutWrapperData,
children: [
{
path: "",
component: LockV2Component,
},
],
},
],
},
{
path: "",
component: AnonLayoutWrapperComponent,

View File

@@ -16,7 +16,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 { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular";
import { LockService, 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";
@@ -117,6 +117,7 @@ import { ForegroundTaskSchedulerService } from "../../platform/services/task-sch
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
@@ -536,6 +537,11 @@ const safeProviders: SafeProvider[] = [
provide: CLIENT_TYPE,
useValue: ClientType.Browser,
}),
safeProvider({
provide: LockComponentService,
useClass: ExtensionLockComponentService,
deps: [],
}),
safeProvider({
provide: Fido2UserVerificationService,
useClass: Fido2UserVerificationService,

View File

@@ -0,0 +1,325 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsService } from "@bitwarden/key-management";
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
import { ExtensionLockComponentService } from "./extension-lock-component.service";
describe("ExtensionLockComponentService", () => {
let service: ExtensionLockComponentService;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let biometricsService: MockProxy<BiometricsService>;
let pinService: MockProxy<PinServiceAbstraction>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let cryptoService: MockProxy<CryptoService>;
let routerService: MockProxy<BrowserRouterService>;
beforeEach(() => {
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
platformUtilsService = mock<PlatformUtilsService>();
biometricsService = mock<BiometricsService>();
pinService = mock<PinServiceAbstraction>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
cryptoService = mock<CryptoService>();
routerService = mock<BrowserRouterService>();
TestBed.configureTestingModule({
providers: [
ExtensionLockComponentService,
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: userDecryptionOptionsService,
},
{
provide: PlatformUtilsService,
useValue: platformUtilsService,
},
{
provide: BiometricsService,
useValue: biometricsService,
},
{
provide: PinServiceAbstraction,
useValue: pinService,
},
{
provide: VaultTimeoutSettingsService,
useValue: vaultTimeoutSettingsService,
},
{
provide: CryptoService,
useValue: cryptoService,
},
{
provide: BrowserRouterService,
useValue: routerService,
},
],
});
service = TestBed.inject(ExtensionLockComponentService);
});
it("instantiates", () => {
expect(service).not.toBeFalsy();
});
describe("getPreviousUrl", () => {
it("returns the previous URL", () => {
routerService.getPreviousUrl.mockReturnValue("previousUrl");
expect(service.getPreviousUrl()).toBe("previousUrl");
});
});
describe("getBiometricsError", () => {
it("returns a biometric error description when given a valid error type", () => {
expect(
service.getBiometricsError({
message: "startDesktop",
}),
).toBe("startDesktopDesc");
});
it("returns null when given an invalid error type", () => {
expect(
service.getBiometricsError({
message: "invalidError",
}),
).toBeNull();
});
it("returns null when given a null input", () => {
expect(service.getBiometricsError(null)).toBeNull();
});
});
describe("isWindowVisible", () => {
it("throws an error", async () => {
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
});
});
describe("getBiometricsUnlockBtnText", () => {
it("returns the biometric unlock button text", () => {
expect(service.getBiometricsUnlockBtnText()).toBe("unlockWithBiometrics");
});
});
describe("getAvailableUnlockOptions$", () => {
interface MockInputs {
hasMasterPassword: boolean;
osSupportsBiometric: boolean;
biometricLockSet: boolean;
hasBiometricEncryptedUserKeyStored: boolean;
platformSupportsSecureStorage: boolean;
pinDecryptionAvailable: boolean;
}
const table: [MockInputs, UnlockOptions][] = [
[
// MP + PIN + Biometrics available
{
hasMasterPassword: true,
osSupportsBiometric: true,
biometricLockSet: true,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: true,
},
{
masterPassword: {
enabled: true,
},
pin: {
enabled: true,
},
biometrics: {
enabled: true,
disableReason: null,
},
},
],
[
// PIN + Biometrics available
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: true,
},
{
masterPassword: {
enabled: false,
},
pin: {
enabled: true,
},
biometrics: {
enabled: true,
disableReason: null,
},
},
],
[
// Biometrics available: user key stored with no secure storage
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: false,
pinDecryptionAvailable: false,
},
{
masterPassword: {
enabled: false,
},
pin: {
enabled: false,
},
biometrics: {
enabled: true,
disableReason: null,
},
},
],
[
// Biometrics available: no user key stored with no secure storage
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
hasBiometricEncryptedUserKeyStored: false,
platformSupportsSecureStorage: false,
pinDecryptionAvailable: false,
},
{
masterPassword: {
enabled: false,
},
pin: {
enabled: false,
},
biometrics: {
enabled: true,
disableReason: null,
},
},
],
[
// Biometrics not available: biometric lock not set
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: false,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
},
{
masterPassword: {
enabled: false,
},
pin: {
enabled: false,
},
biometrics: {
enabled: false,
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
},
},
],
[
// Biometrics not available: user key not stored
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
hasBiometricEncryptedUserKeyStored: false,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
},
{
masterPassword: {
enabled: false,
},
pin: {
enabled: false,
},
biometrics: {
enabled: false,
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
},
},
],
[
// Biometrics not available: OS doesn't support
{
hasMasterPassword: false,
osSupportsBiometric: false,
biometricLockSet: true,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
},
{
masterPassword: {
enabled: false,
},
pin: {
enabled: false,
},
biometrics: {
enabled: false,
disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
},
},
],
];
test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => {
const userId = "userId" as UserId;
const userDecryptionOptions = {
hasMasterPassword: mockInputs.hasMasterPassword,
};
// MP
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
of(userDecryptionOptions),
);
// Biometrics
biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
cryptoService.hasUserKeyStored.mockResolvedValue(
mockInputs.hasBiometricEncryptedUserKeyStored,
);
platformUtilsService.supportsSecureStorage.mockReturnValue(
mockInputs.platformSupportsSecureStorage,
);
// PIN
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
expect(unlockOptions).toEqual(expectedOutput);
});
});
});

View File

@@ -0,0 +1,117 @@
import { inject } from "@angular/core";
import { combineLatest, defer, map, Observable } from "rxjs";
import {
BiometricsDisableReason,
LockComponentService,
UnlockOptions,
} from "@bitwarden/auth/angular";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsService } from "@bitwarden/key-management";
import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors";
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
export class ExtensionLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
private readonly platformUtilsService = inject(PlatformUtilsService);
private readonly biometricsService = inject(BiometricsService);
private readonly pinService = inject(PinServiceAbstraction);
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
private readonly cryptoService = inject(CryptoService);
private readonly routerService = inject(BrowserRouterService);
getPreviousUrl(): string | null {
return this.routerService.getPreviousUrl();
}
getBiometricsError(error: any): string | null {
const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes];
if (!biometricsError) {
return null;
}
return biometricsError.description;
}
async isWindowVisible(): Promise<boolean> {
throw new Error("Method not implemented.");
}
getBiometricsUnlockBtnText(): string {
return "unlockWithBiometrics";
}
private async isBiometricLockSet(userId: UserId): Promise<boolean> {
const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored(
KeySuffixOptions.Biometric,
userId,
);
const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
return (
biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
);
}
private getBiometricsDisabledReason(
osSupportsBiometric: boolean,
biometricLockSet: boolean,
): BiometricsDisableReason | null {
if (!osSupportsBiometric) {
return BiometricsDisableReason.NotSupportedOnOperatingSystem;
} else if (!biometricLockSet) {
return BiometricsDisableReason.EncryptedKeysUnavailable;
}
return null;
}
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
return combineLatest([
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
defer(() => this.biometricsService.supportsBiometric()),
defer(() => this.isBiometricLockSet(userId)),
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
]).pipe(
map(
([
supportsBiometric,
isBiometricsLockSet,
userDecryptionOptions,
pinDecryptionAvailable,
]) => {
const disableReason = this.getBiometricsDisabledReason(
supportsBiometric,
isBiometricsLockSet,
);
const unlockOpts: UnlockOptions = {
masterPassword: {
enabled: userDecryptionOptions.hasMasterPassword,
},
pin: {
enabled: pinDecryptionAvailable,
},
biometrics: {
enabled: supportsBiometric && isBiometricsLockSet,
disableReason: disableReason,
},
};
return unlockOpts;
},
),
);
}
}

View File

@@ -4,7 +4,8 @@
<tools-send-form
formId="sendForm"
[config]="config"
(sendSaved)="onSendSaved()"
(onSendCreated)="onSendCreated($event)"
(onSendUpdated)="onSendUpdated($event)"
[submitBtn]="submitBtn"
>
</tools-send-form>

View File

@@ -2,12 +2,13 @@ import { CommonModule, Location } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Params } from "@angular/router";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { map, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { SendId } from "@bitwarden/common/types/guid";
import {
@@ -95,14 +96,25 @@ export class SendAddEditComponent {
private sendApiService: SendApiService,
private toastService: ToastService,
private dialogService: DialogService,
private router: Router,
) {
this.subscribeToParams();
}
/**
* Handles the event when the send is saved.
* Handles the event when the send is created.
*/
onSendSaved() {
async onSendCreated(send: SendView) {
await this.router.navigate(["/send-created"], {
queryParams: { sendId: send.id },
});
return;
}
/**
* Handles the event when the send is updated.
*/
onSendUpdated(send: SendView) {
this.location.back();
}

View File

@@ -1,6 +1,11 @@
<main class="tw-top-0">
<popup-page>
<popup-header slot="header" [pageTitle]="'createdSend' | i18n" showBackButton>
<popup-header
slot="header"
[pageTitle]="'createdSend' | i18n"
showBackButton
[backAction]="close.bind(this)"
>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>

View File

@@ -1,6 +1,6 @@
import { CommonModule, Location } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, RouterLink } from "@angular/router";
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
@@ -33,6 +33,7 @@ describe("SendCreatedComponent", () => {
let location: MockProxy<Location>;
let activatedRoute: MockProxy<ActivatedRoute>;
let environmentService: MockProxy<EnvironmentService>;
let router: MockProxy<Router>;
const sendId = "test-send-id";
const deletionDate = new Date();
@@ -52,6 +53,7 @@ describe("SendCreatedComponent", () => {
location = mock<Location>();
activatedRoute = mock<ActivatedRoute>();
environmentService = mock<EnvironmentService>();
router = mock<Router>();
Object.defineProperty(environmentService, "environment$", {
configurable: true,
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
@@ -89,6 +91,7 @@ describe("SendCreatedComponent", () => {
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: EnvironmentService, useValue: environmentService },
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
{ provide: Router, useValue: router },
],
}).compileComponents();
});
@@ -109,10 +112,10 @@ describe("SendCreatedComponent", () => {
expect(component["daysAvailable"]).toBe(7);
});
it("should navigate back on close", () => {
it("should navigate back to send list on close", async () => {
fixture.detectChanges();
component.close();
expect(location.back).toHaveBeenCalled();
await component.close();
expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]);
});
describe("getDaysAvailable", () => {

View File

@@ -1,7 +1,7 @@
import { CommonModule, Location } from "@angular/common";
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, RouterLink } from "@angular/router";
import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -30,6 +30,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
PopupHeaderComponent,
PopupPageComponent,
RouterLink,
RouterModule,
PopupFooterComponent,
IconModule,
],
@@ -45,10 +46,11 @@ export class SendCreatedComponent {
private sendService: SendService,
private route: ActivatedRoute,
private toastService: ToastService,
private location: Location,
private router: Router,
private environmentService: EnvironmentService,
) {
const sendId = this.route.snapshot.queryParamMap.get("sendId");
this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => {
this.send = sendViews.find((s) => s.id === sendId);
if (this.send) {
@@ -62,8 +64,8 @@ export class SendCreatedComponent {
return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24)));
}
close() {
this.location.back();
async close() {
await this.router.navigate(["/tabs/send"]);
}
async copyLink() {