From 5af12505f1f97d172d6a7c44a9557b062c406221 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:01:02 -0400 Subject: [PATCH 01/20] Switch userVisibleOnly to `false` (#14202) --- .../notifications/internal/worker-webpush-connection.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts index 74981b6782f..a1143d14d1d 100644 --- a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts @@ -134,7 +134,7 @@ class MyWebPushConnector implements WebPushConnector { private async pushManagerSubscribe(key: string) { return await this.serviceWorkerRegistration.pushManager.subscribe({ - userVisibleOnly: true, + userVisibleOnly: false, applicationServerKey: key, }); } From 7bf4bae7c6d693c8d438aaa082e28a147be85e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 18 Apr 2025 10:43:22 +0200 Subject: [PATCH 02/20] Revert event-driven .show, in order for bootstrap/hide-to-tray to work (#14181) --- apps/desktop/src/main/window.main.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 73231f1f730..1c396b09b21 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -292,11 +292,7 @@ export class WindowMain { this.win.maximize(); } - // Show it later since it might need to be maximized. - // use once event to avoid flash on unstyled content. - this.win.once("ready-to-show", () => { - this.win.show(); - }); + this.win.show(); if (template === "full-app") { // and load the index.html of the app. From d6beca569c4e80bd1657f5e0aca3a9f18ad697c2 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 18 Apr 2025 08:45:05 -0500 Subject: [PATCH 03/20] [PM-19810] Member Access Report - CSV export fix (#14313) --- .../services/member-access-report.mock.ts | 38 +++++++++++++++++++ .../member-access-report.service.spec.ts | 34 ++++++++++++++++- .../services/member-access-report.service.ts | 20 ++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts index 9ace555dd2e..b07e4946ca7 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts @@ -229,3 +229,41 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [ ], } as MemberAccessResponse, ]; + +export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [ + { + userName: "Alice Smith", + email: "asmith@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ + { + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "Alice Group 1", + itemCount: 10, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + ], + } as MemberAccessResponse, + { + userName: "Robert Brown", + email: "rbrown@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "5678", + usesKeyConnector: false, + accessDetails: [] as MemberAccessDetails[], + } as MemberAccessResponse, +]; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 7d6beca48ec..e6efac83616 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -4,7 +4,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; -import { memberAccessReportsMock } from "./member-access-report.mock"; +import { + memberAccessReportsMock, + memberAccessWithoutAccessDetailsReportsMock, +} from "./member-access-report.mock"; import { MemberAccessReportService } from "./member-access-report.service"; describe("ImportService", () => { const mockOrganizationId = "mockOrgId" as OrganizationId; @@ -112,5 +115,34 @@ describe("ImportService", () => { ]), ); }); + + it("should generate user report export items and include users with no access", async () => { + reportApiService.getMemberAccessData.mockImplementation(() => + Promise.resolve(memberAccessWithoutAccessDetailsReportsMock), + ); + const result = + await memberAccessReportService.generateUserReportExportItems(mockOrganizationId); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: "asmith@email.com", + name: "Alice Smith", + twoStepLogin: "memberAccessReportTwoFactorEnabledTrue", + accountRecovery: "memberAccessReportAuthenticationEnabledTrue", + group: "Alice Group 1", + totalItems: "10", + }), + expect.objectContaining({ + email: "rbrown@email.com", + name: "Robert Brown", + twoStepLogin: "memberAccessReportTwoFactorEnabledFalse", + accountRecovery: "memberAccessReportAuthenticationEnabledFalse", + group: "memberAccessReportNoGroup", + totalItems: "0", + }), + ]), + ); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index b7ff5551e2c..029dce8a404 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -65,6 +65,26 @@ export class MemberAccessReportService { } const exportItems = memberAccessReports.flatMap((report) => { + // to include users without access details + // which means a user has no groups, collections or items + if (report.accessDetails.length === 0) { + return [ + { + email: report.email, + name: report.userName, + twoStepLogin: report.twoFactorEnabled + ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") + : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), + accountRecovery: report.accountRecoveryEnabled + ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") + : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), + group: this.i18nService.t("memberAccessReportNoGroup"), + collection: this.i18nService.t("memberAccessReportNoCollection"), + collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"), + totalItems: "0", + }, + ]; + } const userDetails = report.accessDetails.map((detail) => { const collectionName = collectionNameMap.get(detail.collectionName.encryptedString); return { From 9d16435d0858d40783ff8bc5494f27314cf00a0f Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 18 Apr 2025 09:52:12 -0400 Subject: [PATCH 04/20] docs(ViewModel): Add JSDocs to view to explain proper use (#14214) --- libs/common/src/models/view/view.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/models/view/view.ts b/libs/common/src/models/view/view.ts index 1f16b3d5958..2869617dca5 100644 --- a/libs/common/src/models/view/view.ts +++ b/libs/common/src/models/view/view.ts @@ -1 +1,5 @@ +// See https://contributing.bitwarden.com/architecture/clients/data-model/#view for proper use. +// View models represent the decrypted state of a corresponding Domain model. +// They typically match the Domain model but contains a decrypted string for any EncString fields. +// Don't use this to represent arbitrary component view data as that isn't what it is for. export class View {} From e026799071e39219f600c8e1fca9fa024b4be844 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 18 Apr 2025 15:57:27 +0200 Subject: [PATCH 05/20] [PM-13128] Enable Breadcrumb Policies (#13584) * [PM-13128] Enable Breadcrumb Policies * [PM-13128] Enable Breadcrumb Policies * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * remove dead code * wip * wip * wip * refactor * Fix for providers * revert to functional auth guard * change prerequisite to info variant * address comment * r * r * r * tests * r * r * fix tests * feedback * fix tests * fix tests * Rename upselling to breadcrumbing * Address feedback * Fix build & tests * Make the guard callback use Observable instead of a promise * Pm 13128 suggestions (#14041) * Rename new enum value * Show the upgrade button when breadcrumbing is enabled * Show mouse pointer when cursor is hovered above badge * Do not make the dialogs overlap * Align badge middle * Gap * Badge should be a `button` instead of `span` * missing button@type --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Alex Morask --- .../guards/org-permissions.guard.spec.ts | 30 +++- .../guards/org-permissions.guard.ts | 26 ++- .../organization-layout.component.html | 2 +- .../layouts/organization-layout.component.ts | 15 ++ .../policies/policies.component.html | 15 +- .../policies/policies.component.ts | 61 +++++-- .../policies/policy-edit.component.html | 18 +++ .../policies/policy-edit.component.ts | 33 +++- .../organization-settings-routing.module.ts | 13 +- .../layouts/header/web-header.component.html | 2 +- .../src/services/jslib-services.module.ts | 1 + .../organization-billing.service.ts | 9 ++ .../organization-billing.service.spec.ts | 149 ++++++++++++++++++ .../services/organization-billing.service.ts | 33 +++- 14 files changed, 380 insertions(+), 27 deletions(-) create mode 100644 libs/common/src/billing/services/organization-billing.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts index 9afd34ca149..d628e23063f 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts @@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => { it("permits navigation if the user has permissions", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => true); + permissionsCallback.mockReturnValue(true); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard(permissionsCallback)(route, state), ); - expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId })); + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles a Promise returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(Promise.resolve(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles an Observable returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(of(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); expect(actual).toBe(true); }); describe("if the user does not have permissions", () => { it("and there is no Item ID, block navigation", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => false); + permissionsCallback.mockReturnValue(false); state = mock({ root: mock({ diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts index d399f9c9c05..6c9090a27b4 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts @@ -1,13 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { inject } from "@angular/core"; +import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, } from "@angular/router"; -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs"; import { canAccessOrgAdmin, @@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components"; * proceeds as expected. */ export function organizationPermissionsGuard( - permissionsCallback?: (organization: Organization) => boolean, + permissionsCallback?: ( + organization: Organization, + ) => boolean | Promise | Observable, ): CanActivateFn { return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const router = inject(Router); @@ -51,6 +53,7 @@ export function organizationPermissionsGuard( const i18nService = inject(I18nService); const syncService = inject(SyncService); const accountService = inject(AccountService); + const environmentInjector = inject(EnvironmentInjector); // TODO: We need to fix issue once and for all. if ((await syncService.getLastSync()) == null) { @@ -78,7 +81,22 @@ export function organizationPermissionsGuard( return router.createUrlTree(["/"]); } - const hasPermissions = permissionsCallback == null || permissionsCallback(org); + if (permissionsCallback == null) { + // No additional permission checks required, allow navigation + return true; + } + + const callbackResult = runInInjectionContext(environmentInjector, () => + permissionsCallback(org), + ); + + const hasPermissions = isObservable(callbackResult) + ? await firstValueFrom(callbackResult) // handles observables + : await Promise.resolve(callbackResult); // handles promises and boolean values + + if (hasPermissions !== true && hasPermissions !== false) { + throw new Error("Permission callback did not resolve to a boolean."); + } if (!hasPermissions) { // Handle linkable ciphers for organizations the user only has view access to diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index e50c55e83d2..fec790dabcb 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -97,7 +97,7 @@ ; protected isBreadcrumbEventLogsEnabled$: Observable; protected showSponsoredFamiliesDropdown$: Observable; + protected canShowPoliciesTab$: Observable; constructor( private route: ActivatedRoute, @@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit { protected bannerService: AccountDeprovisioningBannerService, private accountService: AccountService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit { )) ? "claimedDomains" : "domainVerification"; + + this.canShowPoliciesTab$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService + .isBreadcrumbingPoliciesEnabled$(organization) + .pipe( + map( + (isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies, + ), + ), + ), + ); } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 24021bb765f..e40b9d80e9e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,4 +1,17 @@ - + + @let organization = organization$ | async; + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 52cb4da107a..2b86d76d9b1 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, lastValueFrom } from "rxjs"; -import { first, map } from "rxjs/operators"; +import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs"; +import { first } from "rxjs/operators"; import { getOrganizationById, @@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { DialogService } from "@bitwarden/components"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component"; +import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { PolicyListService } from "../../core/policy-list.service"; import { BasePolicy } from "../policies"; +import { CollectionDialogTabType } from "../shared/components/collection-dialog"; import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component"; @@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit { loading = true; organizationId: string; policies: BasePolicy[]; - organization: Organization; + protected organization$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); + protected isBreadcrumbingEnabled$: Observable; constructor( private route: ActivatedRoute, - private organizationService: OrganizationService, private accountService: AccountService, + private organizationService: OrganizationService, private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, + private organizationBillingService: OrganizationBillingServiceAbstraction, private dialogService: DialogService, ) {} @@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit { const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - this.organization = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); + this.organization$ = this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)); this.policies = this.policyListService.getPolicies(); await this.load(); @@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit { this.orgPolicies.forEach((op) => { this.policiesEnabledMap.set(op.type, op.enabled); }); - + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); this.loading = false; } @@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); - if (result === PolicyEditDialogResult.Saved) { - await this.load(); + switch (result) { + case PolicyEditDialogResult.Saved: + await this.load(); + break; + case PolicyEditDialogResult.UpgradePlan: + await this.changePlan(await firstValueFrom(this.organization$)); + break; } } + + protected readonly CollectionDialogTabType = CollectionDialogTabType; + protected readonly All = All; + + protected async changePlan(organization: Organization) { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: organization.id, + subscription: null, + productTierType: organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + + await this.load(); + } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 671083a2318..7f33f08f888 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -1,5 +1,17 @@
+ + +
+ + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index f33460e8c16..49f4d15a100 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -9,12 +9,20 @@ import { ViewContainerRef, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Observable, map } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -35,6 +43,7 @@ export type PolicyEditDialogData = { export enum PolicyEditDialogResult { Saved = "saved", + UpgradePlan = "upgrade-plan", } @Component({ selector: "app-policy-edit", @@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit { loading = true; enabled = false; saveDisabled$: Observable; - defaultTypes: any[]; policyComponent: BasePolicyComponent; private policyResponse: PolicyResponse; formGroup = this.formBuilder.group({ enabled: [this.enabled], }); + protected organization$: Observable; + protected isBreadcrumbingEnabled$: Observable; + constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, + private organizationService: OrganizationService, private i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, private toastService: ToastService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} + get policy(): BasePolicy { return this.data.policy; } @@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit { throw e; } } + this.organization$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.organizationService.organizations$(userId)), + getOrganizationById(this.data.organizationId), + ); + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); } submit = async () => { @@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit { static open = (dialogService: DialogService, config: DialogConfig) => { return dialogService.open(PolicyEditComponent, config); }; + + protected upgradePlan(): void { + this.dialogRef.close(PolicyEditDialogResult.UpgradePlan); + } } diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index a644086628c..cfec0be531b 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -1,8 +1,10 @@ -import { NgModule } from "@angular/core"; +import { NgModule, inject } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; @@ -41,7 +43,14 @@ const routes: Routes = [ { path: "policies", component: PoliciesComponent, - canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)], + canActivate: [ + organizationPermissionsGuard((o: Organization) => { + const organizationBillingService = inject(OrganizationBillingServiceAbstraction); + return organizationBillingService + .isBreadcrumbingPoliciesEnabled$(o) + .pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled)); + }), + ], data: { titleId: "policies", }, diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 28d786f2d64..4b0da4ae569 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -12,7 +12,7 @@

diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3cce9b5357e..8e2b3409593 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [ I18nServiceAbstraction, OrganizationApiServiceAbstraction, SyncService, + ConfigService, ], }), safeProvider({ diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 69309014fac..8024a120b0a 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,5 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Observable } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; @@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction { organizationId: string, subscription: SubscriptionInformation, ) => Promise; + + /** + * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. + * @param organization + */ + abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable; } diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts new file mode 100644 index 00000000000..7b194dff637 --- /dev/null +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -0,0 +1,149 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { KeyService } from "@bitwarden/key-management"; + +describe("BillingAccountProfileStateService", () => { + let apiService: jest.Mocked; + let billingApiService: jest.Mocked; + let keyService: jest.Mocked; + let encryptService: jest.Mocked; + let i18nService: jest.Mocked; + let organizationApiService: jest.Mocked; + let syncService: jest.Mocked; + let configService: jest.Mocked; + + let sut: OrganizationBillingService; + + beforeEach(() => { + apiService = mock(); + billingApiService = mock(); + keyService = mock(); + encryptService = mock(); + i18nService = mock(); + organizationApiService = mock(); + syncService = mock(); + configService = mock(); + + sut = new OrganizationBillingService( + apiService, + billingApiService, + keyService, + encryptService, + i18nService, + organizationApiService, + syncService, + configService, + ); + }); + + afterEach(() => { + return jest.resetAllMocks(); + }); + + describe("isBreadcrumbingPoliciesEnabled", () => { + it("returns false when feature flag is disabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when organization belongs to a provider", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: true, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("returns false when cannot edit subscription", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: false, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it.each([ + ["Teams", ProductTierType.Teams], + ["TeamsStarter", ProductTierType.TeamsStarter], + ])("returns true when all conditions are met with %s tier", async (_, productTierType) => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: productTierType, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(true); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when product tier is not supported", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Enterprise, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("handles all conditions false correctly", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: true, + canEditSubscription: false, + productTierType: ProductTierType.Free, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("verifies feature flag is only called once", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 83efbf0a30c..6622cdcdce3 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,5 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Observable, of, switchMap } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -20,7 +25,7 @@ import { PlanInformation, SubscriptionInformation, } from "../abstractions"; -import { PlanType } from "../enums"; +import { PlanType, ProductTierType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; @@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private i18nService: I18nService, private organizationApiService: OrganizationApiService, private syncService: SyncService, + private configService: ConfigService, ) {} async getPaymentSource(organizationId: string): Promise { @@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); await this.billingApiService.restartSubscription(organizationId, request); } + + isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable { + if (organization === null || organization === undefined) { + return of(false); + } + + return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe( + switchMap((featureFlagEnabled) => { + if (!featureFlagEnabled) { + return of(false); + } + + if (organization.isProviderUser || !organization.canEditSubscription) { + return of(false); + } + + const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter]; + const isSupportedProduct = supportedProducts.some( + (product) => product === organization.productTierType, + ); + + return of(isSupportedProduct); + }), + ); + } } From cbab354c0eaecc461c33f6d616fcc212b519bca2 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Fri, 18 Apr 2025 10:38:19 -0400 Subject: [PATCH 06/20] Hide bit-icon component from screen readers by default (#14295) * adds aria-hidden to bit-icon when no aria-label provided * add ariaLabel to logo svg usages * add ariaLabel documentation * default ariaLable value to undefined * add logo label to translations * adds i18n pipe to component * Add binding to example docs --- apps/browser/src/_locales/en/messages.json | 3 +++ .../extension-anon-layout-wrapper.component.html | 7 ++++++- .../extension-anon-layout-wrapper.component.ts | 2 ++ .../accept-family-sponsorship.component.html | 3 ++- apps/web/src/locales/en/messages.json | 3 +++ .../manage/accept-provider.component.html | 6 +++++- .../providers/setup/setup-provider.component.html | 6 +++++- .../setup/setup-business-unit.component.html | 6 +++++- .../angular/anon-layout/anon-layout.component.html | 2 +- libs/components/src/icon/icon.component.ts | 14 +++++++++----- libs/components/src/icon/icon.mdx | 10 ++++++++++ libs/components/src/icon/icon.stories.ts | 4 ++++ 12 files changed, 55 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 87b94650b51..b0f3fb5d19e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2,6 +2,9 @@ "appName": { "message": "Bitwarden" }, + "appLogoLabel": { + "message": "Bitwarden logo" + }, "extName": { "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 54cb5203a87..bd2886dacf0 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -5,7 +5,12 @@ [showBackButton]="showBackButton" [pageTitle]="''" > - + diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 51dbb6503d7..d6cccf31bb4 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -12,6 +12,7 @@ import { } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Icon, IconModule, Translation } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -36,6 +37,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { AnonLayoutComponent, CommonModule, CurrentAccountComponent, + I18nPipe, IconModule, PopOutComponent, PopupPageComponent, diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index fdbb6dbba91..ca1264829b9 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -1,6 +1,7 @@
- + +
- +

- +

- +

- +

NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an + > `ariaLabel` is explicitly provided to the `` component + ```html ``` + With `ariaLabel` + + ```html + + ``` + 8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client which supports multiple style modes. diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 53454567b7f..7892bdd3ec1 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -26,5 +26,9 @@ export const Default: Story = { mapping: GenericIcons, control: { type: "select" }, }, + ariaLabel: { + control: "text", + description: "the text used by a screen reader to describe the icon", + }, }, }; From 4da03821cee44184c8567f93131818340a50a5fd Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:23:52 +0200 Subject: [PATCH 07/20] move key connector components to KM team ownership (#14008) --- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 2 +- apps/browser/src/popup/app.module.ts | 2 +- apps/cli/src/auth/commands/unlock.command.ts | 2 +- .../convert-to-key-connector.command.ts | 0 apps/desktop/src/app/app-routing.module.ts | 2 +- apps/desktop/src/app/app.module.ts | 2 +- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- apps/web/src/app/oss-routing.module.ts | 2 +- apps/web/src/app/shared/loose-components.module.ts | 2 +- libs/key-management-ui/src/index.ts | 1 + .../src/key-connector}/remove-password.component.ts | 0 16 files changed, 11 insertions(+), 10 deletions(-) rename apps/browser/src/{auth/popup => key-management/key-connector}/remove-password.component.html (100%) rename apps/browser/src/{auth/popup => key-management/key-connector}/remove-password.component.ts (80%) rename apps/cli/src/{commands => key-management}/convert-to-key-connector.command.ts (100%) rename apps/desktop/src/{auth => key-management/key-connector}/remove-password.component.html (100%) rename apps/desktop/src/{auth => key-management/key-connector}/remove-password.component.ts (80%) rename apps/web/src/app/{auth => key-management/key-connector}/remove-password.component.html (100%) rename apps/web/src/app/{auth => key-management/key-connector}/remove-password.component.ts (80%) rename libs/{angular/src/auth/components => key-management-ui/src/key-connector}/remove-password.component.ts (100%) diff --git a/apps/browser/src/auth/popup/remove-password.component.html b/apps/browser/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/browser/src/auth/popup/remove-password.component.html rename to apps/browser/src/key-management/key-connector/remove-password.component.html diff --git a/apps/browser/src/auth/popup/remove-password.component.ts b/apps/browser/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/browser/src/auth/popup/remove-password.component.ts rename to apps/browser/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/browser/src/auth/popup/remove-password.component.ts +++ b/apps/browser/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 21ac4c19700..45955506b91 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -55,7 +55,6 @@ import { ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperData, } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -65,6 +64,7 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 0d392afa63b..8bea41da4d6 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -25,13 +25,13 @@ import { import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 6d524759dd6..d10c3f38ebd 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -17,7 +17,7 @@ import { MasterKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; -import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command"; +import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command"; import { Response } from "../../models/response"; import { MessageResponse } from "../../models/response/message.response"; import { CliUtils } from "../../utils"; diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/key-management/convert-to-key-connector.command.ts similarity index 100% rename from apps/cli/src/commands/convert-to-key-connector.command.ts rename to apps/cli/src/key-management/convert-to-key-connector.command.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index cd5064a87e4..0c6bc730c2c 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -50,9 +50,9 @@ import { import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index fdc25ed642e..b892324a979 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -13,11 +13,11 @@ import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginModule } from "../auth/login/login.module"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { SshAgentService } from "../autofill/services/ssh-agent.service"; import { PremiumComponent } from "../billing/app/accounts/premium.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component"; import { AddEditComponent } from "../vault/app/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/app/vault/attachments.component"; diff --git a/apps/desktop/src/auth/remove-password.component.html b/apps/desktop/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/desktop/src/auth/remove-password.component.html rename to apps/desktop/src/key-management/key-connector/remove-password.component.html diff --git a/apps/desktop/src/auth/remove-password.component.ts b/apps/desktop/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/desktop/src/auth/remove-password.component.ts rename to apps/desktop/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/desktop/src/auth/remove-password.component.ts +++ b/apps/desktop/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/auth/remove-password.component.html b/apps/web/src/app/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/web/src/app/auth/remove-password.component.html rename to apps/web/src/app/key-management/key-connector/remove-password.component.html diff --git a/apps/web/src/app/auth/remove-password.component.ts b/apps/web/src/app/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/web/src/app/auth/remove-password.component.ts rename to apps/web/src/app/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/web/src/app/auth/remove-password.component.ts +++ b/apps/web/src/app/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 0334519516a..fc8356505f5 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -58,7 +58,6 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; -import { RemovePasswordComponent } from "./auth/remove-password.component"; import { SetPasswordComponent } from "./auth/set-password.component"; import { AccountComponent } from "./auth/settings/account/account.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; @@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { RouteDataProperties } from "./core"; +import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 469ebe457d0..eb63b9f798c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -15,7 +15,6 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/ import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { AccountComponent } from "../auth/settings/account/account.component"; import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component"; @@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; import { SelectableAvatarComponent } from "../components/selectable-avatar.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "../layouts/frontend-layout.component"; import { HeaderModule } from "../layouts/header/header.module"; import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module"; diff --git a/libs/key-management-ui/src/index.ts b/libs/key-management-ui/src/index.ts index 2f98538caad..b330e390d36 100644 --- a/libs/key-management-ui/src/index.ts +++ b/libs/key-management-ui/src/index.ts @@ -7,3 +7,4 @@ export { LockComponentService, UnlockOptions } from "./lock/services/lock-compon export { KeyRotationTrustInfoComponent } from "./key-rotation/key-rotation-trust-info.component"; export { AccountRecoveryTrustComponent } from "./trust/account-recovery-trust.component"; export { EmergencyAccessTrustComponent } from "./trust/emergency-access-trust.component"; +export { RemovePasswordComponent } from "./key-connector/remove-password.component"; diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/key-management-ui/src/key-connector/remove-password.component.ts similarity index 100% rename from libs/angular/src/auth/components/remove-password.component.ts rename to libs/key-management-ui/src/key-connector/remove-password.component.ts From da57b944ae77e29c5032af5f27c35a5eae2f6644 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:47:04 -0400 Subject: [PATCH 08/20] chore(deps): Remove Platform from owning Cargo.lock --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f402b15dd5..de28b210887 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,8 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev -## No ownership for Cargo.toml to allow dependency updates +## No ownership fo Cargo.lock and Cargo.toml to allow dependency updates +apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml ## Auth team files ## From a82996526279bc683a31c6c944810b37a7836fef Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 18 Apr 2025 13:05:58 -0500 Subject: [PATCH 09/20] [PM-20386] valuesChanges returns a string (#14338) --- .../vault-export-ui/src/components/export.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 4e9b4175838..71599c19ae0 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -220,7 +220,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(([value]) => { + .subscribe((value) => { this.organizationId = value !== "myVault" ? value : undefined; this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip"); From f86a5c2b6ea4bf18194f3c5875c8c1e63ea3f36f Mon Sep 17 00:00:00 2001 From: Chase Nelson Date: Fri, 18 Apr 2025 14:55:23 -0400 Subject: [PATCH 10/20] [PM-19798] [PM-18807] Fix base64 encoding/decoding with special characters (#14089) * Refactor base64 encoding/decoding to use BufferLib * Add tests for base64 encoding and decoding functions --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- libs/common/src/platform/misc/utils.spec.ts | 69 +++++++++++++++++++++ libs/common/src/platform/misc/utils.ts | 4 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 964a2a19413..818138863fb 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -706,4 +706,73 @@ describe("Utils Service", () => { }); }); }); + + describe("fromUtf8ToB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromUtf8ToB64(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromUtf8ToB64(asciiHelloWorld); + expect(str).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("should convert various special characters", () => { + const cases = [ + { input: "»", output: "wrs=" }, + { input: "¦", output: "wqY=" }, + { input: "£", output: "wqM=" }, + { input: "é", output: "w6k=" }, + { input: "ö", output: "w7Y=" }, + { input: "»»", output: "wrvCuw==" }, + ]; + cases.forEach((c) => { + const utfStr = c.input; + const str = Utils.fromUtf8ToB64(utfStr); + expect(str).toBe(c.output); + }); + }); + }); + + describe("fromB64ToUtf8(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromB64ToUtf8(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromB64ToUtf8(b64HelloWorldString); + expect(str).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should handle various special characters", () => { + const cases = [ + { input: "wrs=", output: "»" }, + { input: "wqY=", output: "¦" }, + { input: "wqM=", output: "£" }, + { input: "w6k=", output: "é" }, + { input: "w7Y=", output: "ö" }, + { input: "wrvCuw==", output: "»»" }, + ]; + + cases.forEach((c) => { + const b64Str = c.input; + const str = Utils.fromB64ToUtf8(b64Str); + expect(str).toBe(c.output); + }); + }); + }); }); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index ef65d2130a0..203a04851c5 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -233,7 +233,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(utfStr, "utf8").toString("base64"); } else { - return decodeURIComponent(escape(Utils.global.btoa(utfStr))); + return BufferLib.from(utfStr, "utf8").toString("base64"); } } @@ -245,7 +245,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(b64Str, "base64").toString("utf8"); } else { - return decodeURIComponent(escape(Utils.global.atob(b64Str))); + return BufferLib.from(b64Str, "base64").toString("utf8"); } } From c798e1f10d365d584eb75fda40fc93a8115e10ad Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:21:01 +0100 Subject: [PATCH 11/20] [PM-18250]Do not default account credit amount (#13719) * Set the default value to zero * Remove the 20 dollar for org --- .../web/src/app/billing/shared/add-credit-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index bfe811f1aa7..45dab542ce8 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -76,7 +76,7 @@ export class AddCreditDialogComponent implements OnInit { async ngOnInit() { if (this.organizationId != null) { if (this.creditAmount == null) { - this.creditAmount = "20.00"; + this.creditAmount = "0.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; const userId = await firstValueFrom( @@ -93,7 +93,7 @@ export class AddCreditDialogComponent implements OnInit { } } else { if (this.creditAmount == null) { - this.creditAmount = "10.00"; + this.creditAmount = "0.00"; } const [userId, email] = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), From d4dd8d096b607447a932163119d0fc3fe45f7906 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:35:42 -0400 Subject: [PATCH 12/20] chore(builds): [PM-20431] Add bit-web to build-web workflow paths * Add bit-web to build-web workflow paths. * Updated to also include bit-common --- .github/workflows/build-cli.yml | 6 ++++-- .github/workflows/build-web.yml | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e113d5c253b..e89ca59a297 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -12,12 +12,13 @@ on: - 'cf-pages' paths: - 'apps/cli/**' + - 'bitwarden_license/bit-cli/**' + - 'bitwarden_license/bit-common/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' push: branches: - 'main' @@ -25,12 +26,13 @@ on: - 'hotfix-rc-cli' paths: - 'apps/cli/**' + - 'bitwarden_license/bit-cli/**' + - 'bitwarden_license/bit-common/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' workflow_call: inputs: {} workflow_dispatch: diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 3da524702fe..fb429468134 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -12,6 +12,8 @@ on: - 'cf-pages' paths: - 'apps/web/**' + - 'bitwarden_license/bit-common/**' + - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' @@ -24,6 +26,8 @@ on: - 'hotfix-rc-web' paths: - 'apps/web/**' + - 'bitwarden_license/bit-common/**' + - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' From 5dfa0eb91a014eb3418b0f1ef56a31d55ba06e75 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 19 Apr 2025 16:10:57 +0200 Subject: [PATCH 13/20] [PM-20277] Disable fingerprint on org invite (#14285) --- .../accept-organization.service.spec.ts | 7 ------- .../accept-organization.service.ts | 11 ----------- 2 files changed, 18 deletions(-) diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index cc2d0e371ff..67dbee223d1 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -24,7 +24,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component"; import { I18nService } from "../../core/i18n.service"; import { @@ -200,11 +199,6 @@ describe("AcceptOrganizationInviteService", () => { encryptedString: "encryptedString", } as EncString); - jest.mock("../../admin-console/organizations/manage/organization-trust.component"); - OrganizationTrustComponent.open = jest.fn().mockReturnValue({ - closed: new BehaviorSubject(true), - }); - await globalState.update(() => invite); policyService.getResetPasswordPolicyOptions.mockReturnValue([ @@ -217,7 +211,6 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(OrganizationTrustComponent.open).toHaveBeenCalled(); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( { key: "userKey" }, Utils.fromB64ToArray("publicKey"), diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index 8b5db9f4872..b6a7719c548 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -31,8 +31,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component"; - import { OrganizationInvite } from "./organization-invite"; // We're storing the organization invite for 2 reasons: @@ -189,15 +187,6 @@ export class AcceptOrganizationInviteService { } const publicKey = Utils.fromB64ToArray(response.publicKey); - const dialogRef = OrganizationTrustComponent.open(this.dialogService, { - name: invite.organizationName, - orgId: invite.organizationId, - publicKey, - }); - const result = await firstValueFrom(dialogRef.closed); - if (result !== true) { - throw new Error("Organization not trusted, aborting user key rotation"); - } const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); From 8de42caf0406aac7ebaa9a3e7654cf7a76604d34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:07:20 +0200 Subject: [PATCH 14/20] [deps] KM: Update Rust crate rsa to v0.9.8 (#12298) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bernd Schoolmann --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 04bc4887ff7..0884f59e863 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -2455,9 +2455,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 01838359147..dc08c2e5a1b 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -38,7 +38,7 @@ oslog = "=0.2.0" pin-project = "=1.1.8" pkcs8 = "=0.10.2" rand = "=0.8.5" -rsa = "=0.9.6" +rsa = "=0.9.8" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" security-framework = "=3.1.0" From 86b0a6aa35c3b4bfdca4a3441ad14fd3d929c68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 21 Apr 2025 12:21:00 +0200 Subject: [PATCH 15/20] Support for logging from NAPI (#14335) * Support for log to electron console from NAPI * Fix test mock --- apps/desktop/desktop_native/Cargo.lock | 1 + apps/desktop/desktop_native/napi/Cargo.toml | 1 + apps/desktop/desktop_native/napi/index.d.ts | 10 ++++ apps/desktop/desktop_native/napi/src/lib.rs | 58 +++++++++++++++++++ .../services/electron-log.main.service.ts | 24 ++++++++ .../services/electron-log.service.spec.ts | 8 +++ 6 files changed, 102 insertions(+) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0884f59e863..6858011e0cc 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -987,6 +987,7 @@ dependencies = [ "base64", "desktop_core", "hex", + "log", "napi", "napi-build", "napi-derive", diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index d59581a5e2e..669f166e748 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -18,6 +18,7 @@ base64 = { workspace = true } hex = { workspace = true } anyhow = { workspace = true } desktop_core = { path = "../core" } +log = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 6ded4e3db93..952f2571c5d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -185,3 +185,13 @@ export declare namespace crypto { export declare namespace passkey_authenticator { export function register(): void } +export declare namespace logging { + export const enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4 + } + export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 8cbc526487e..37796ef6f59 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -807,3 +807,61 @@ pub mod passkey_authenticator { }) } } + +#[napi] +pub mod logging { + use log::{Level, Metadata, Record}; + use napi::threadsafe_function::{ + ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use std::sync::OnceLock; + struct JsLogger(OnceLock>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From for LogLevel { + fn from(level: Level) -> Self { + match level { + Level::Trace => LogLevel::Trace, + Level::Debug => LogLevel::Debug, + Level::Info => LogLevel::Info, + Level::Warn => LogLevel::Warn, + Level::Error => LogLevel::Error, + } + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) { + let _ = JS_LOGGER.0.set(js_log_fn); + let _ = log::set_logger(&JS_LOGGER); + log::set_max_level(log::LevelFilter::Debug); + } + + impl log::Log for JsLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + let Some(logger) = self.0.get() else { + return; + }; + let msg = (record.level().into(), record.args().to_string()); + let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking); + } + + fn flush(&self) {} + } +} diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index d7100b54825..947f4449271 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -7,6 +7,7 @@ import log from "electron-log/main"; import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum"; import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { logging } from "@bitwarden/desktop-napi"; import { isDev } from "../../utils"; @@ -30,6 +31,29 @@ export class ElectronLogMainService extends BaseLogService { ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { this.write(level, message, ...optionalParams); }); + + logging.initNapiLog((error, level, message) => this.writeNapiLog(level, message)); + } + + private writeNapiLog(level: logging.LogLevel, message: string) { + let levelType: LogLevelType; + + switch (level) { + case logging.LogLevel.Debug: + levelType = LogLevelType.Debug; + break; + case logging.LogLevel.Warn: + levelType = LogLevelType.Warning; + break; + case logging.LogLevel.Error: + levelType = LogLevelType.Error; + break; + default: + levelType = LogLevelType.Info; + break; + } + + this.write(levelType, "[NAPI] " + message); } write(level: LogLevelType, message?: any, ...optionalParams: any[]) { diff --git a/apps/desktop/src/platform/services/electron-log.service.spec.ts b/apps/desktop/src/platform/services/electron-log.service.spec.ts index 918508977fd..db3093e08e2 100644 --- a/apps/desktop/src/platform/services/electron-log.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-log.service.spec.ts @@ -5,6 +5,14 @@ jest.mock("electron", () => ({ ipcMain: { handle: jest.fn(), on: jest.fn() }, })); +jest.mock("@bitwarden/desktop-napi", () => { + return { + logging: { + initNapiLog: jest.fn(), + }, + }; +}); + describe("ElectronLogMainService", () => { it("sets dev based on electron method", () => { process.env.ELECTRON_IS_DEV = "1"; From 201bdf752b3ff57dba3581d13c026d55db12cdb5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 21 Apr 2025 14:14:13 +0200 Subject: [PATCH 16/20] [PM-19728] Device bulk get keys during key rotation (#14216) * Add support for device list endpoint keys during key rotation * Update libs/common/src/auth/abstractions/devices/responses/device.response.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../auth/abstractions/devices/responses/device.response.ts | 5 +++++ .../services/device-trust.service.implementation.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts index 84a2fb03c28..6b7f17f65ce 100644 --- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts @@ -15,6 +15,9 @@ export class DeviceResponse extends BaseResponse { creationDate: string; revisionDate: string; isTrusted: boolean; + encryptedUserKey: string | null; + encryptedPublicKey: string | null; + devicePendingAuthRequest: DevicePendingAuthRequest | null; constructor(response: any) { @@ -27,6 +30,8 @@ export class DeviceResponse extends BaseResponse { this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); this.isTrusted = this.getResponseProperty("IsTrusted"); + this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey"); + this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey"); this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest"); } } diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index a2211753f4e..c82efa0c571 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -209,9 +209,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { devices.data .filter((device) => device.isTrusted) .map(async (device) => { - const deviceWithKeys = await this.devicesApiService.getDeviceKeys(device.identifier); const publicKey = await this.encryptService.decryptToBytes( - deviceWithKeys.encryptedPublicKey, + new EncString(device.encryptedPublicKey), oldUserKey, ); From 83d7ea6aa3e989faef02e3885d4c40693e72a9ac Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 21 Apr 2025 09:52:53 -0400 Subject: [PATCH 17/20] [PM-20334] Remove Bindgen from Windows Plugin Authenticator (#14328) * PM-20334: Draft work removing bindgen * PM-20334: Remove comments and address clippy concerns * PM-20334: Edit wpa readme and remove .hpp header file --- apps/desktop/desktop_native/Cargo.lock | 66 ----- .../windows_plugin_authenticator/Cargo.toml | 3 - .../windows_plugin_authenticator/README.md | 20 +- .../windows_plugin_authenticator/build.rs | 27 -- .../pluginauthenticator.hpp | 231 ------------------ .../windows_plugin_authenticator/src/lib.rs | 61 +++-- .../windows_plugin_authenticator/src/pa.rs | 15 -- 7 files changed, 49 insertions(+), 374 deletions(-) delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/build.rs delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 6858011e0cc..c786264a563 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -410,26 +410,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - [[package]] name = "bitflags" version = "2.8.0" @@ -573,15 +553,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -622,17 +593,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.31" @@ -1493,15 +1453,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.14" @@ -2303,16 +2254,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2491,12 +2432,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -3736,7 +3671,6 @@ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" name = "windows_plugin_authenticator" version = "0.0.0" dependencies = [ - "bindgen", "hex", "windows 0.61.1", "windows-core 0.61.0", diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index 72a8505389e..18abb86d057 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -5,9 +5,6 @@ edition = { workspace = true } license = { workspace = true } publish = { workspace = true } -[target.'cfg(target_os = "windows")'.build-dependencies] -bindgen = { workspace = true } - [target.'cfg(windows)'.dependencies] windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader" ] } windows-core = { workspace = true } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/README.md b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md index 6dc72ceed46..4802c5d4243 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/README.md +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md @@ -2,22 +2,6 @@ This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's. +This crate is very much a WIP and is not ready for internal use. + You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn). - -## Building - -To build this crate, set the following environment variables: - -- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang)) - -### Bash Example - -``` -export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' -``` - -### PowerShell Example - -``` -$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' -``` diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs deleted file mode 100644 index 4843145bac0..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -fn main() { - #[cfg(target_os = "windows")] - windows(); -} - -#[cfg(target_os = "windows")] -fn windows() { - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); - - let bindings = bindgen::Builder::default() - .header("pluginauthenticator.hpp") - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .allowlist_type("DWORD") - .allowlist_type("PBYTE") - .allowlist_type("EXPERIMENTAL.*") - .allowlist_function(".*EXPERIMENTAL.*") - .allowlist_function("WebAuthNGetApiVersionNumber") - .generate() - .expect("Unable to generate bindings."); - - bindings - .write_to_file(format!( - "{}\\windows_plugin_authenticator_bindings.rs", - out_dir - )) - .expect("Couldn't write bindings."); -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp b/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp deleted file mode 100644 index c800266a3e6..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp +++ /dev/null @@ -1,231 +0,0 @@ -/* - Bitwarden's pluginauthenticator.hpp - - Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h - - This is a C++ header file, so the extension has been manually - changed from `.h` to `.hpp`, so bindgen will automatically - generate the correct C++ bindings. - - More Info: https://rust-lang.github.io/rust-bindgen/cpp.html -*/ - -/* this ALWAYS GENERATED file contains the definitions for the interfaces */ - -/* File created by MIDL compiler version 8.01.0628 */ -/* @@MIDL_FILE_HEADING( ) */ - -/* verify that the version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCNDR_H_VERSION__ -#define __REQUIRED_RPCNDR_H_VERSION__ 501 -#endif - -/* verify that the version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCSAL_H_VERSION__ -#define __REQUIRED_RPCSAL_H_VERSION__ 100 -#endif - -#include "rpc.h" -#include "rpcndr.h" - -#ifndef __RPCNDR_H_VERSION__ -#error this stub requires an updated version of -#endif /* __RPCNDR_H_VERSION__ */ - -#ifndef COM_NO_WINDOWS_H -#include "windows.h" -#include "ole2.h" -#endif /*COM_NO_WINDOWS_H*/ - -#ifndef __pluginauthenticator_h__ -#define __pluginauthenticator_h__ - -#if defined(_MSC_VER) && (_MSC_VER >= 1020) -#pragma once -#endif - -#ifndef DECLSPEC_XFGVIRT -#if defined(_CONTROL_FLOW_GUARD_XFG) -#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) -#else -#define DECLSPEC_XFGVIRT(base, func) -#endif -#endif - -/* Forward Declarations */ - -#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ -#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ -typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator; - -#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */ - -/* header files for imported files */ -#include "oaidl.h" -#include "webauthn.h" - -#ifdef __cplusplus -extern "C"{ -#endif - -/* interface __MIDL_itf_pluginauthenticator_0000_0000 */ -/* [local] */ - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST - { - HWND hWnd; - GUID transactionId; - DWORD cbRequestSignature; - /* [size_is] */ byte *pbRequestSignature; - DWORD cbEncodedRequest; - /* [size_is] */ byte *pbEncodedRequest; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE - { - DWORD cbEncodedResponse; - /* [size_is] */ byte *pbEncodedResponse; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST - { - GUID transactionId; - DWORD cbRequestSignature; - /* [size_is] */ byte *pbRequestSignature; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec; - -#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ -#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ - -/* interface EXPERIMENTAL_IPluginAuthenticator */ -/* [unique][version][uuid][object] */ - -EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator; - -#if defined(__cplusplus) && !defined(CINTERFACE) - - MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998") - EXPERIMENTAL_IPluginAuthenticator : public IUnknown - { - public: - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; - - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; - - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0; - - }; - -#else /* C style interface */ - - typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl - { - BEGIN_INTERFACE - - DECLSPEC_XFGVIRT(IUnknown, QueryInterface) - HRESULT ( STDMETHODCALLTYPE *QueryInterface )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in REFIID riid, - /* [annotation][iid_is][out] */ - _COM_Outptr_ void **ppvObject); - - DECLSPEC_XFGVIRT(IUnknown, AddRef) - ULONG ( STDMETHODCALLTYPE *AddRef )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); - - DECLSPEC_XFGVIRT(IUnknown, Release) - ULONG ( STDMETHODCALLTYPE *Release )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request); - - END_INTERFACE - } EXPERIMENTAL_IPluginAuthenticatorVtbl; - - interface EXPERIMENTAL_IPluginAuthenticator - { - CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl; - }; - -#ifdef COBJMACROS - - -#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \ - ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) - -#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \ - ( (This)->lpVtbl -> AddRef(This) ) - -#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \ - ( (This)->lpVtbl -> Release(This) ) - - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) ) - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) ) - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) ) - -#endif /* COBJMACROS */ - -#endif /* C style interface */ - -#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */ - -/* Additional Prototypes for ALL interfaces */ - -unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); -void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * ); - -unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); -void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * ); - -/* end of Additional Prototypes */ - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index fe2e35df2f8..21257068bd0 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -2,15 +2,6 @@ #![allow(non_snake_case)] #![allow(non_camel_case_types)] -mod pa; - -use pa::{ - DWORD, EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, - EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, - EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, PBYTE, -}; use std::ffi::c_uchar; use std::ptr; use windows::Win32::Foundation::*; @@ -23,11 +14,53 @@ const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator"; const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; const RPID: &str = "bitwarden.com"; -/// Returns the current Windows WebAuthN version. -pub fn get_version_number() -> u32 { - unsafe { pa::WebAuthNGetApiVersionNumber() } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + pub transactionId: GUID, + pub cbRequestSignature: Dword, + pub pbRequestSignature: *mut byte, } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST { + pub hWnd: HWND, + pub transactionId: GUID, + pub cbRequestSignature: Dword, + pub pbRequestSignature: *mut byte, + pub cbEncodedRequest: Dword, + pub pbEncodedRequest: *mut byte, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { + pub cbOpSignPubKey: Dword, + pub pbOpSignPubKey: PByte, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + pub cbEncodedResponse: Dword, + pub pbEncodedResponse: *mut byte, +} + +type Dword = u32; +type Byte = u8; +type byte = u8; +pub type PByte = *mut Byte; + +type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST = + *const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; +pub type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST = + *const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; +pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE = + *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; +pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = + *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; + /// Handles initialization and registration for the Bitwarden desktop app as a /// plugin authenticator with Windows. /// For now, also adds the authenticator @@ -123,9 +156,9 @@ fn add_authenticator() -> std::result::Result<(), String> { pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(), }; - let plugin_signing_public_key_byte_count: DWORD = 0; + let plugin_signing_public_key_byte_count: Dword = 0; let mut plugin_signing_public_key: c_uchar = 0; - let plugin_signing_public_key_ptr: PBYTE = &mut plugin_signing_public_key; + let plugin_signing_public_key_ptr: PByte = &mut plugin_signing_public_key; let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { cbOpSignPubKey: plugin_signing_public_key_byte_count, diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs deleted file mode 100644 index 7c93399594d..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - The 'pa' (plugin authenticator) module will contain the generated - bindgen code. - - The attributes below will suppress warnings from the generated code. -*/ - -#![cfg(target_os = "windows")] -#![allow(clippy::all)] -#![allow(warnings)] - -include!(concat!( - env!("OUT_DIR"), - "/windows_plugin_authenticator_bindings.rs" -)); From 1b6f75806d17c4f2733d15789afa6ec708d49e9a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 21 Apr 2025 14:22:41 +0000 Subject: [PATCH 18/20] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index b311b837e78..9ed3c807c11 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.3.2", + "version": "2025.4.0", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 35578fb8321..fca62568048 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.3.2", + "version": "2025.4.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 556a73ac15b..47e0cc465ef 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.3.2", + "version": "2025.4.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 04fe4290d31..1bf6a1d41a1 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.3.0", + "version": "2025.4.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ae34deee5b8..9f442da47a1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.4.1", + "version": "2025.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index a720dff7257..f6449bd9626 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index e288f5f5a79..45a6f6b90af 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.4.1", + "version": "2025.4.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 1f4ae9c29cf..e65848602e9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.4.0", + "version": "2025.4.1", "scripts": { "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 65f0c87721a..3e16fd7ba68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,11 +191,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.3.2" + "version": "2025.4.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.3.0", + "version": "2025.4.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -231,7 +231,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -245,7 +245,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.4.0" + "version": "2025.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From a250395e6d0e4efb9404a120a30c5e9730f1bae0 Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Mon, 21 Apr 2025 15:36:36 +0100 Subject: [PATCH 19/20] =?UTF-8?q?BRE-757:=20add=20hold=20label=20for=20Ren?= =?UTF-8?q?ovate=20PR=20that=20touches=20production=20workf=E2=80=A6=20(#1?= =?UTF-8?q?4311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index c4202ed2a68..ee97f16b0a9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -49,6 +49,7 @@ "./github/workflows/release-web.yml", ], commitMessagePrefix: "[deps] BRE:", + addLabels: ["hold"], }, { // Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular. From 43b1f5536024693c5de969b9109ae0f123d567d3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 21 Apr 2025 16:57:26 +0200 Subject: [PATCH 20/20] [PM-18697] Remove old symmetric key representations in symmetriccryptokey (#13598) * Remove AES128CBC-HMAC encryption * Increase test coverage * Refactor symmetric keys and increase test coverage * Re-add type 0 encryption * Fix ts strict warning * Remove old symmetric key representations in symmetriccryptokey * Fix desktop build * Fix test * Fix build * Update libs/common/src/key-management/crypto/services/web-crypto-function.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/node/src/services/node-crypto-function.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Undo changes * Remove cast * Undo changes to tests * Fix linting * Undo removing new Uint8Array in aesDecryptFastParameters * Fix merge conflicts * Fix test * Fix another test * Fix test --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../main-biometrics.service.spec.ts | 2 +- .../os-biometrics-windows.service.ts | 10 +++- .../auth-request/auth-request.service.spec.ts | 6 ++- .../auth-request/auth-request.service.ts | 6 ++- .../encrypt.service.implementation.ts | 24 +++++---- .../crypto/services/encrypt.service.spec.ts | 19 +++++-- .../services/web-crypto-function.service.ts | 50 ++++++++----------- .../services/key-connector.service.spec.ts | 8 ++- .../services/key-connector.service.ts | 8 ++- .../domain/symmetric-crypto-key.spec.ts | 18 ++----- .../models/domain/symmetric-crypto-key.ts | 26 +--------- .../services/node-crypto-function.service.ts | 40 +++++++++------ 12 files changed, 111 insertions(+), 106 deletions(-) diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts index 88dd8c60ed5..09a4dcef4b3 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts @@ -300,7 +300,7 @@ describe("MainBiometricsService", function () { expect(userKey).not.toBeNull(); expect(userKey!.keyB64).toBe(biometricKey); - expect(userKey!.encType).toBe(EncryptionType.AesCbc256_HmacSha256_B64); + expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( "Bitwarden_biometric", `${userId}_user_biometric`, diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts index cd8c94329bc..53647549295 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts @@ -1,5 +1,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { biometrics, passwords } from "@bitwarden/desktop-napi"; @@ -218,7 +220,13 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { symmetricKey: SymmetricCryptoKey, clientKeyPartB64: string | undefined, ): biometrics.KeyMaterial { - const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64; + let key = null; + const innerKey = symmetricKey.inner(); + if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + key = Utils.fromBufferToB64(innerKey.authenticationKey); + } else { + key = Utils.fromBufferToB64(innerKey.encryptionKey); + } const result = { osKeyPartB64: key, diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index b5846fcfdbf..f7bf2260a36 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -106,7 +106,9 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); + masterPasswordService.masterKeySubject.next( + new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, + ); masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); await sut.approveOrDenyAuthRequest( @@ -115,7 +117,7 @@ describe("AuthRequestService", () => { ); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( - { encKey: new Uint8Array(64) }, + new SymmetricCryptoKey(new Uint8Array(32)), expect.anything(), ); }); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index f4316c2e519..226403d9c8c 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -14,6 +14,7 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, StateProvider, @@ -120,7 +121,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { keyToEncrypt = await this.keyService.getUserKey(); } - const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned( + keyToEncrypt as SymmetricCryptoKey, + pubKey, + ); const response = new PasswordlessAuthRequest( encryptedKey.encryptedString, diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 4b299c9c6e6..e9d8c1c30a2 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -47,7 +47,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { throw new Error("Type 0 encryption is not supported."); } } @@ -84,7 +84,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { throw new Error("Type 0 encryption is not supported."); } } @@ -124,7 +124,7 @@ export class EncryptServiceImplementation implements EncryptService { if (encString.encryptionType !== innerKey.type) { this.logDecryptError( "Key encryption type does not match payload encryption type", - key.encType, + innerKey.type, encString.encryptionType, decryptContext, ); @@ -148,7 +148,7 @@ export class EncryptServiceImplementation implements EncryptService { if (!macsEqual) { this.logMacFailed( "decryptToUtf8 MAC comparison failed. Key or payload has changed.", - key.encType, + innerKey.type, encString.encryptionType, decryptContext, ); @@ -191,7 +191,7 @@ export class EncryptServiceImplementation implements EncryptService { if (encThing.encryptionType !== inner.type) { this.logDecryptError( "Encryption key type mismatch", - key.encType, + inner.type, encThing.encryptionType, decryptContext, ); @@ -200,19 +200,23 @@ export class EncryptServiceImplementation implements EncryptService { if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) { if (encThing.macBytes == null) { - this.logDecryptError("Mac missing", key.encType, encThing.encryptionType, decryptContext); + this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext); return null; } const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength); macData.set(new Uint8Array(encThing.ivBytes), 0); macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength); - const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); + const computedMac = await this.cryptoFunctionService.hmac( + macData, + inner.authenticationKey, + "sha256", + ); const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { this.logMacFailed( "MAC comparison failed. Key or payload has changed.", - key.encType, + inner.type, encThing.encryptionType, decryptContext, ); @@ -222,14 +226,14 @@ export class EncryptServiceImplementation implements EncryptService { return await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey, + inner.encryptionKey, "cbc", ); } else if (inner.type === EncryptionType.AesCbc256_B64) { return await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey, + inner.encryptionKey, "cbc", ); } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 4cbe3a3da90..fb55fa92605 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -6,7 +6,10 @@ import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + Aes256CbcHmacKey, + SymmetricCryptoKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { makeStaticByteArray } from "../../../../spec"; @@ -64,6 +67,10 @@ describe("EncryptService", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); await expect(encryptService.encrypt(null!, key)).rejects.toThrow( "Type 0 encryption is not supported.", @@ -146,6 +153,10 @@ describe("EncryptService", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); mock32Key.key = makeStaticByteArray(32); + mock32Key.inner.mockReturnValue({ + type: 0, + encryptionKey: mock32Key.key, + }); await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( "Type 0 encryption is not supported.", @@ -228,7 +239,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey), + expect.toEqualBuffer(key.inner().encryptionKey), "cbc", ); @@ -249,7 +260,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey), + expect.toEqualBuffer(key.inner().encryptionKey), "cbc", ); @@ -267,7 +278,7 @@ describe("EncryptService", () => { expect(cryptoFunctionService.hmac).toBeCalledWith( expect.toEqualBuffer(expectedMacData), - key.macKey, + (key.inner() as Aes256CbcHmacKey).authenticationKey, "sha256", ); diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts index 0c80d508b2d..430774ca2ed 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts @@ -1,6 +1,7 @@ import * as argon2 from "argon2-browser"; import * as forge from "node-forge"; +import { EncryptionType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { CbcDecryptParameters, @@ -247,37 +248,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService { mac: string | null, key: SymmetricCryptoKey, ): CbcDecryptParameters { - const p = {} as CbcDecryptParameters; - if (key.meta != null) { - p.encKey = key.meta.encKeyByteString; - p.macKey = key.meta.macKeyByteString; + const innerKey = key.inner(); + if (innerKey.type === EncryptionType.AesCbc256_B64) { + return { + iv: forge.util.decode64(iv), + data: forge.util.decode64(data), + encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(), + } as CbcDecryptParameters; + } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + const macData = forge.util.decode64(iv) + forge.util.decode64(data); + return { + iv: forge.util.decode64(iv), + data: forge.util.decode64(data), + encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(), + macKey: forge.util.createBuffer(innerKey.authenticationKey).getBytes(), + mac: forge.util.decode64(mac!), + macData, + } as CbcDecryptParameters; + } else { + throw new Error("Unsupported encryption type."); } - - if (p.encKey == null) { - p.encKey = forge.util.decode64(key.encKeyB64); - } - p.data = forge.util.decode64(data); - p.iv = forge.util.decode64(iv); - p.macData = p.iv + p.data; - if (p.macKey == null && key.macKeyB64 != null) { - p.macKey = forge.util.decode64(key.macKeyB64); - } - if (mac != null) { - p.mac = forge.util.decode64(mac); - } - - // cache byte string keys for later - if (key.meta == null) { - key.meta = {}; - } - if (key.meta.encKeyByteString == null) { - key.meta.encKeyByteString = p.encKey; - } - if (p.macKey != null && key.meta.macKeyByteString == null) { - key.meta.macKeyByteString = p.macKey; - } - - return p; } aesDecryptFast({ diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts index fd3ce0c4777..b88ada56129 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.spec.ts @@ -252,7 +252,9 @@ describe("KeyConnectorService", () => { const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); masterPasswordService.masterKeySubject.next(masterKey); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); @@ -273,7 +275,9 @@ describe("KeyConnectorService", () => { // Arrange const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); const masterKey = getMockMasterKey(); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); const error = new Error("Failed to post user key to key connector"); organizationService.organizations$.mockReturnValue(of([organization])); diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index 91b8e9100ac..9799f06f64a 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -95,7 +95,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; const organization = await this.getManagingOrganization(userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); try { await this.apiService.postUserKeyToKeyConnector( @@ -157,7 +159,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.tokenService.getEmail(), kdfConfig, ); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const keyConnectorRequest = new KeyConnectorUserKeyRequest( + Utils.fromBufferToB64(masterKey.inner().encryptionKey), + ); await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.keyService.makeUserKey(masterKey); diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts index cce99b847bb..6b641ad443a 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts @@ -2,7 +2,7 @@ import { makeStaticByteArray } from "../../../../spec"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; +import { Aes256CbcHmacKey, SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("SymmetricCryptoKey", () => { it("errors if no key", () => { @@ -19,13 +19,8 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - encKey: key, - encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - encType: EncryptionType.AesCbc256_B64, key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - macKey: null, - macKeyB64: undefined, innerKey: { type: EncryptionType.AesCbc256_B64, encryptionKey: key, @@ -38,14 +33,9 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - encKey: key.slice(0, 32), - encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", - encType: EncryptionType.AesCbc256_HmacSha256_B64, key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==", - macKey: key.slice(32, 64), - macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=", innerKey: { type: EncryptionType.AesCbc256_HmacSha256_B64, encryptionKey: key.slice(0, 32), @@ -86,8 +76,8 @@ describe("SymmetricCryptoKey", () => { expect(actual).toEqual({ type: EncryptionType.AesCbc256_HmacSha256_B64, - encryptionKey: key.encKey, - authenticationKey: key.macKey, + encryptionKey: key.inner().encryptionKey, + authenticationKey: (key.inner() as Aes256CbcHmacKey).authenticationKey, }); }); @@ -95,7 +85,7 @@ describe("SymmetricCryptoKey", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const actual = key.toEncoded(); - expect(actual).toEqual(key.encKey); + expect(actual).toEqual(key.inner().encryptionKey); }); it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => { diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index 45e15c1f602..c85f3432b28 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -25,15 +25,7 @@ export class SymmetricCryptoKey { private innerKey: Aes256CbcHmacKey | Aes256CbcKey; key: Uint8Array; - encKey: Uint8Array; - macKey?: Uint8Array; - encType: EncryptionType; - keyB64: string; - encKeyB64: string; - macKeyB64: string; - - meta: any; /** * @param key The key in one of the permitted serialization formats @@ -48,30 +40,16 @@ export class SymmetricCryptoKey { type: EncryptionType.AesCbc256_B64, encryptionKey: key, }; - this.encType = EncryptionType.AesCbc256_B64; this.key = key; - this.keyB64 = Utils.fromBufferToB64(this.key); - - this.encKey = key; - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - - this.macKey = null; - this.macKeyB64 = undefined; + this.keyB64 = this.toBase64(); } else if (key.byteLength === 64) { this.innerKey = { type: EncryptionType.AesCbc256_HmacSha256_B64, encryptionKey: key.slice(0, 32), authenticationKey: key.slice(32), }; - this.encType = EncryptionType.AesCbc256_HmacSha256_B64; this.key = key; - this.keyB64 = Utils.fromBufferToB64(this.key); - - this.encKey = key.slice(0, 32); - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - - this.macKey = key.slice(32); - this.macKeyB64 = Utils.fromBufferToB64(this.macKey); + this.keyB64 = this.toBase64(); } else { throw new Error(`Unsupported encType/key length ${key.byteLength}`); } diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index 78d72d44104..33ea3adf357 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -3,6 +3,7 @@ import * as crypto from "crypto"; import * as forge from "node-forge"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CbcDecryptParameters, @@ -172,24 +173,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { mac: string | null, key: SymmetricCryptoKey, ): CbcDecryptParameters { - const p = {} as CbcDecryptParameters; - p.encKey = key.encKey; - p.data = Utils.fromB64ToArray(data); - p.iv = Utils.fromB64ToArray(iv); + const dataBytes = Utils.fromB64ToArray(data); + const ivBytes = Utils.fromB64ToArray(iv); + const macBytes = mac != null ? Utils.fromB64ToArray(mac) : null; - const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength); - macData.set(new Uint8Array(p.iv), 0); - macData.set(new Uint8Array(p.data), p.iv.byteLength); - p.macData = macData; + const innerKey = key.inner(); - if (key.macKey != null) { - p.macKey = key.macKey; + if (innerKey.type === EncryptionType.AesCbc256_B64) { + return { + iv: ivBytes, + data: dataBytes, + encKey: innerKey.encryptionKey, + } as CbcDecryptParameters; + } else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { + const macData = new Uint8Array(ivBytes.byteLength + dataBytes.byteLength); + macData.set(new Uint8Array(ivBytes), 0); + macData.set(new Uint8Array(dataBytes), ivBytes.byteLength); + return { + iv: ivBytes, + data: dataBytes, + mac: macBytes, + macData: macData, + encKey: innerKey.encryptionKey, + macKey: innerKey.authenticationKey, + } as CbcDecryptParameters; + } else { + throw new Error("Unsupported encryption type"); } - if (mac != null) { - p.mac = Utils.fromB64ToArray(mac); - } - - return p; } async aesDecryptFast({