From 9b2fbdba1c028bf3394064609630d2ec224baefa Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Thu, 16 Oct 2025 12:08:19 -0400 Subject: [PATCH 1/5] [PM-26947] Fix Billing Address Defect (#16872) * fix(billing): Add Billing Address component * fix(billing): Update tax refresh logic and swap billing address component * fix(billing): Fix headers * fix(billing): Do not show bank payment option for premium upgrade --- .../upgrade-payment.component.html | 10 +++- .../upgrade-payment.component.ts | 54 +++++++++++-------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html index 7b92ae10947..fad883f942a 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html @@ -31,11 +31,19 @@ }
+
{{ "paymentMethod" | i18n }}
+
{{ "billingAddress" | i18n }}
+ +
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index 33568435d01..bd88c22f98d 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -10,7 +10,7 @@ import { } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { debounceTime, Observable } from "rxjs"; +import { catchError, debounceTime, from, Observable, of, switchMap } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -20,7 +20,10 @@ import { LogService } from "@bitwarden/logging"; import { CartSummaryComponent, LineItem } from "@bitwarden/pricing"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { EnterPaymentMethodComponent } from "../../../payment/components"; +import { + EnterBillingAddressComponent, + EnterPaymentMethodComponent, +} from "../../../payment/components"; import { BillingServicesModule } from "../../../services"; import { SubscriptionPricingService } from "../../../services/subscription-pricing.service"; import { BitwardenSubscriber } from "../../../types"; @@ -65,6 +68,7 @@ export type UpgradePaymentParams = { CartSummaryComponent, ButtonModule, EnterPaymentMethodComponent, + EnterBillingAddressComponent, BillingServicesModule, ], providers: [UpgradePaymentService], @@ -83,6 +87,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { protected formGroup = new FormGroup({ organizationName: new FormControl("", [Validators.required]), paymentForm: EnterPaymentMethodComponent.getFormGroup(), + billingAddress: EnterBillingAddressComponent.getFormGroup(), }); protected loading = signal(true); @@ -140,9 +145,16 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { } }); - this.formGroup.valueChanges - .pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => this.refreshSalesTax()); + this.formGroup.controls.billingAddress.valueChanges + .pipe( + debounceTime(1000), + // Only proceed when form has required values + switchMap(() => this.refreshSalesTax$()), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((tax) => { + this.estimatedTax = tax; + }); this.loading.set(false); } @@ -199,8 +211,8 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { private async processUpgrade(): Promise { // Get common values - const country = this.formGroup.value?.paymentForm?.billingAddress?.country; - const postalCode = this.formGroup.value?.paymentForm?.billingAddress?.postalCode; + const country = this.formGroup.value?.billingAddress?.country; + const postalCode = this.formGroup.value?.billingAddress?.postalCode; if (!this.selectedPlan) { throw new Error("No plan selected"); @@ -246,19 +258,20 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { } } - private async refreshSalesTax(): Promise { + // Create an observable for tax calculation + private refreshSalesTax$(): Observable { const billingAddress = { - country: this.formGroup.value.paymentForm?.billingAddress?.country, - postalCode: this.formGroup.value.paymentForm?.billingAddress?.postalCode, + country: this.formGroup.value?.billingAddress?.country, + postalCode: this.formGroup.value?.billingAddress?.postalCode, }; if (!this.selectedPlan || !billingAddress.country || !billingAddress.postalCode) { - this.estimatedTax = 0; - return; + return of(0); } - this.upgradePaymentService - .calculateEstimatedTax(this.selectedPlan, { + // Convert Promise to Observable + return from( + this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, { line1: null, line2: null, city: null, @@ -266,17 +279,16 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { country: billingAddress.country, postalCode: billingAddress.postalCode, taxId: null, - }) - .then((tax) => { - this.estimatedTax = tax; - }) - .catch((error: unknown) => { + }), + ).pipe( + catchError((error: unknown) => { this.logService.error("Tax calculation failed:", error); this.toastService.showToast({ variant: "error", message: this.i18nService.t("taxCalculationError"), }); - this.estimatedTax = 0; - }); + return of(0); // Return default value on error + }), + ); } } From 94cb1fe07b29cf2b95b1a0684995c55bbb60e9a5 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Thu, 16 Oct 2025 14:30:10 -0400 Subject: [PATCH 2/5] feat(auth-tech-debt): [PM-24103] Remove Get User Key to UserKey$ (#16589) * fix(auth-tech-debt): [PM-24103] Remove Get User Key to UserKey$ - Fixed and updated tests. * fix(auth-tech-debt): [PM-24103] Remove Get User Key to UserKey$ - Fixed test variable being made more vague. --- apps/browser/src/background/main.background.ts | 1 + apps/cli/src/auth/commands/login.command.ts | 2 +- .../src/service-container/service-container.ts | 1 + .../services/emergency-access.service.spec.ts | 6 ++++-- .../services/emergency-access.service.ts | 12 +++++++++--- .../emergency-access.component.ts | 16 ++++++++++++++-- .../src/auth/components/set-pin.component.ts | 2 +- .../src/services/jslib-services.module.ts | 1 + .../auth-request.service.abstraction.ts | 1 + .../login-strategies/login.strategy.spec.ts | 10 ++++++---- .../common/login-strategies/login.strategy.ts | 6 +++++- .../auth-request/auth-request.service.spec.ts | 16 +++++++++++----- .../auth-request/auth-request.service.ts | 11 +++++++++-- ...et-enrollment.service.implementation.spec.ts | 4 ++-- ...d-reset-enrollment.service.implementation.ts | 17 +++++++++++------ 15 files changed, 77 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4cd61ebead1..3ccd78db18c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -886,6 +886,7 @@ export default class MainBackground { this.apiService, this.stateProvider, this.authRequestApiService, + this.accountService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index e1a3d123441..aa43b353f9c 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -621,7 +621,7 @@ export class LoginCommand { const newPasswordHash = await this.keyService.hashMasterKey(masterPassword, newMasterKey); // Grab user key - const userKey = await this.keyService.getUserKey(); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); if (!userKey) { throw new Error("User key not found."); } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index c677e705ec1..ccce00fabd8 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -657,6 +657,7 @@ export class ServiceContainer { this.apiService, this.stateProvider, this.authRequestApiService, + this.accountService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 2ff38f6eab0..05d6094745c 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -17,6 +17,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, UserPrivateKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { newGuid } from "@bitwarden/guid"; import { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type"; @@ -44,6 +45,7 @@ describe("EmergencyAccessService", () => { const mockNewUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("trustedPublicKey")]; + const mockUserId = newGuid() as UserId; beforeAll(() => { emergencyAccessApiService = mock(); @@ -125,7 +127,7 @@ describe("EmergencyAccessService", () => { "mockUserPublicKeyEncryptedUserKey", ); - keyService.getUserKey.mockResolvedValueOnce(mockUserKey); + keyService.userKey$.mockReturnValue(of(mockUserKey)); encryptService.encapsulateKeyUnsigned.mockResolvedValueOnce( mockUserPublicKeyEncryptedUserKey, @@ -134,7 +136,7 @@ describe("EmergencyAccessService", () => { emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce(); // Act - await emergencyAccessService.confirm(id, granteeId, publicKey); + await emergencyAccessService.confirm(id, granteeId, publicKey, mockUserId); // Assert expect(emergencyAccessApiService.postEmergencyAccessConfirm).toHaveBeenCalledWith(id, { diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index cce8d9345b2..b91bc932e83 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -175,11 +175,17 @@ export class EmergencyAccessService * Step 3 of the 3 step setup flow. * Intended for grantor. * @param id emergency access id - * @param token secret token provided in email + * @param granteeId id of the grantee * @param publicKey public key of grantee + * @param activeUserId the active user's id */ - async confirm(id: string, granteeId: string, publicKey: Uint8Array): Promise { - const userKey = await this.keyService.getUserKey(); + async confirm( + id: string, + granteeId: string, + publicKey: Uint8Array, + activeUserId: UserId, + ): Promise { + const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); if (!userKey) { throw new Error("No user key found"); } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index de30205e6fe..f6594f4b11a 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { lastValueFrom, Observable, firstValueFrom, switchMap } from "rxjs"; +import { lastValueFrom, Observable, firstValueFrom, switchMap, map } from "rxjs"; import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -165,7 +165,15 @@ export class EmergencyAccessComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === EmergencyAccessConfirmDialogResult.Confirmed) { - await this.emergencyAccessService.confirm(contact.id, contact.granteeId, publicKey); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + await this.emergencyAccessService.confirm( + contact.id, + contact.granteeId, + publicKey, + activeUserId, + ); updateUser(); this.toastService.showToast({ variant: "success", @@ -176,10 +184,14 @@ export class EmergencyAccessComponent implements OnInit { return; } + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); this.actionPromise = this.emergencyAccessService.confirm( contact.id, contact.granteeId, publicKey, + activeUserId, ); await this.actionPromise; updateUser(); diff --git a/libs/angular/src/auth/components/set-pin.component.ts b/libs/angular/src/auth/components/set-pin.component.ts index 9e351990fff..ba816d5ad34 100644 --- a/libs/angular/src/auth/components/set-pin.component.ts +++ b/libs/angular/src/auth/components/set-pin.component.ts @@ -47,7 +47,7 @@ export class SetPinComponent implements OnInit { } const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userKey = await this.keyService.getUserKey(); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); const userKeyEncryptedPin = await this.pinService.createUserKeyEncryptedPin( pinFormControl.value, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 164f125a5de..f1d1c678c24 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1291,6 +1291,7 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, StateProvider, AuthRequestApiServiceAbstraction, + AccountServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 10cc643fd45..1bfbfd8d004 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -55,6 +55,7 @@ export abstract class AuthRequestServiceAbstraction { * Approve or deny an auth request. * @param approve True to approve, false to deny. * @param authRequest The auth request to approve or deny, must have an id and key. + * @param activeUserId the active user id * @returns The updated auth request, the `requestApproved` field will be true if * approval was successful. * @throws If the auth request is missing an id or key. diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index e2f326d836d..a23f8034238 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -337,7 +337,7 @@ describe("LoginStrategy", () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.privateKey = null; keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); - keyService.getUserKey.mockResolvedValue(userKey); + keyService.userKey$.mockReturnValue(new BehaviorSubject(userKey).asObservable()); apiService.postIdentityToken.mockResolvedValue(tokenResponse); masterPasswordService.masterKeySubject.next(masterKey); @@ -356,9 +356,11 @@ describe("LoginStrategy", () => { }); it("throws if userKey is CoseEncrypt0 (V2 encryption) in createKeyPairForOldAccount", async () => { - keyService.getUserKey.mockResolvedValue({ - inner: () => ({ type: 7 }), - } as UserKey); + keyService.userKey$.mockReturnValue( + new BehaviorSubject({ + inner: () => ({ type: 7 }), + } as unknown as UserKey).asObservable(), + ); await expect(passwordLoginStrategy["createKeyPairForOldAccount"](userId)).resolves.toBe( undefined, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 93198f992cb..7ad5cd24353 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -306,7 +306,11 @@ export abstract class LoginStrategy { protected async createKeyPairForOldAccount(userId: UserId) { try { - const userKey = await this.keyService.getUserKey(userId); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + if (userKey === null) { + throw new Error("User key is null when creating key pair for old account"); + } + if (userKey.inner().type == EncryptionType.CoseEncrypt0) { throw new Error("Cannot create key pair for account on V2 encryption"); } 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 69f38f40989..8cb0cc279ae 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 @@ -1,7 +1,8 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; @@ -9,11 +10,11 @@ import { FakeMasterPasswordService } from "@bitwarden/common/key-management/mast import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { newGuid } from "@bitwarden/guid"; import { KeyService } from "@bitwarden/key-management"; import { DefaultAuthRequestApiService } from "./auth-request-api.service"; @@ -29,10 +30,11 @@ describe("AuthRequestService", () => { const encryptService = mock(); const apiService = mock(); const authRequestApiService = mock(); + const accountService = mock(); let mockPrivateKey: Uint8Array; let mockPublicKey: Uint8Array; - const mockUserId = Utils.newGuid() as UserId; + const mockUserId = newGuid() as UserId; beforeEach(() => { jest.clearAllMocks(); @@ -46,6 +48,7 @@ describe("AuthRequestService", () => { apiService, stateProvider, authRequestApiService, + accountService, ); mockPrivateKey = new Uint8Array(64); @@ -95,6 +98,8 @@ describe("AuthRequestService", () => { const authRequestNoId = new AuthRequestResponse({ id: "", key: "KEY" }); const authRequestNoPublicKey = new AuthRequestResponse({ id: "123", publicKey: "" }); + accountService.activeAccount$ = of({ id: mockUserId } as any); + await expect(sut.approveOrDenyAuthRequest(true, authRequestNoId)).rejects.toThrow( "Auth request has no id", ); @@ -104,8 +109,9 @@ describe("AuthRequestService", () => { }); it("should use the user key if the master key and hash do not exist", async () => { - keyService.getUserKey.mockResolvedValueOnce( - new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, + accountService.activeAccount$ = of({ id: mockUserId } as any); + keyService.userKey$.mockReturnValue( + of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey), ); await sut.approveOrDenyAuthRequest( 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 5fc28e960a8..ba4b9eaf174 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 @@ -4,9 +4,11 @@ import { Observable, Subject, defer, firstValueFrom, map } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -56,6 +58,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private apiService: ApiService, private stateProvider: StateProvider, private authRequestApiService: AuthRequestApiServiceAbstraction, + private accountService: AccountService, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable(); @@ -124,15 +127,19 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { approve: boolean, authRequest: AuthRequestResponse, ): Promise { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + if (!authRequest.id) { throw new Error("Auth request has no id"); } if (!authRequest.publicKey) { throw new Error("Auth request has no public key"); } + if (activeUserId == null) { + throw new Error("User ID is required"); + } const pubKey = Utils.fromB64ToArray(authRequest.publicKey); - - const keyToEncrypt = await this.keyService.getUserKey(); + const keyToEncrypt = await firstValueFrom(this.keyService.userKey$(activeUserId)); const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey); const response = new PasswordlessAuthRequest( diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index d5b787d69f0..7e6e0d53f57 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -103,7 +103,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); - keyService.getUserKey.mockResolvedValue({ key: "key" } as any); + keyService.userKey$.mockReturnValue(of({ key: "key" } as any)); encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedKey as any); await service.enroll("orgId"); diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index f491d7d5eb0..55644009f16 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -8,9 +8,11 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; @@ -43,7 +45,7 @@ export class PasswordResetEnrollmentServiceImplementation async enroll(organizationId: string): Promise; async enroll(organizationId: string, userId: string, userKey: UserKey): Promise; - async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise { + async enroll(organizationId: string, activeUserId?: string, userKey?: UserKey): Promise { const orgKeyResponse = await this.organizationApiService.getKeys(organizationId); if (orgKeyResponse == null) { throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); @@ -51,9 +53,12 @@ export class PasswordResetEnrollmentServiceImplementation const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey); - userId = - userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); - userKey = userKey ?? (await this.keyService.getUserKey(userId)); + activeUserId = + activeUserId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId))); + if (activeUserId == null) { + throw new Error("User ID is required"); + } + userKey = userKey ?? (await firstValueFrom(this.keyService.userKey$(activeUserId as UserId))); // RSA Encrypt user's userKey.key with organization public key const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, orgPublicKey); @@ -62,7 +67,7 @@ export class PasswordResetEnrollmentServiceImplementation await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( organizationId, - userId, + activeUserId, resetRequest, ); } From 9afaa6efc0b48d10a6f0f16bde5481c8fec4e28d Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 16 Oct 2025 21:06:45 +0000 Subject: [PATCH 3/5] Bumped Desktop client to 2025.10.2 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 17dad010e46..19ab9e783d4 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.10.1", + "version": "2025.10.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index bf7789578d9..88be6ebd4f5 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.10.1", + "version": "2025.10.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.10.1", + "version": "2025.10.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 5ca46f87bb7..d122978f943 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.10.1", + "version": "2025.10.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 8da94affe5d..2c1ca659d99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -279,7 +279,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.10.1", + "version": "2025.10.2", "hasInstallScript": true, "license": "GPL-3.0" }, From 7cd9832034c8e0c4ced11af156330e844da225f2 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:07:28 -0700 Subject: [PATCH 4/5] [BEEEP] Use tracing in process_isolation (#16762) --- .../desktop_native/core/src/process_isolation/linux.rs | 9 +++++---- .../desktop_native/core/src/process_isolation/macos.rs | 6 ++---- .../desktop_native/core/src/process_isolation/windows.rs | 6 ++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs index 395d722ea01..bad348c93e2 100644 --- a/apps/desktop/desktop_native/core/src/process_isolation/linux.rs +++ b/apps/desktop/desktop_native/core/src/process_isolation/linux.rs @@ -2,6 +2,7 @@ use anyhow::Result; #[cfg(target_env = "gnu")] use libc::c_uint; use libc::{self, c_int}; +use tracing::info; // RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes // https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20 @@ -20,7 +21,7 @@ pub fn disable_coredumps() -> Result<()> { rlim_cur: 0, rlim_max: 0, }; - println!("[Process Isolation] Disabling core dumps via setrlimit"); + info!("Disabling core dumps via setrlimit."); if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 { let e = std::io::Error::last_os_error(); @@ -48,9 +49,9 @@ pub fn is_core_dumping_disabled() -> Result { pub fn isolate_process() -> Result<()> { let pid = std::process::id(); - println!( - "[Process Isolation] Disabling ptrace and memory access for main ({}) via PR_SET_DUMPABLE", - pid + info!( + pid, + "Disabling ptrace and memory access for main via PR_SET_DUMPABLE." ); if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 { diff --git a/apps/desktop/desktop_native/core/src/process_isolation/macos.rs b/apps/desktop/desktop_native/core/src/process_isolation/macos.rs index ce42e06a832..928eac749c0 100644 --- a/apps/desktop/desktop_native/core/src/process_isolation/macos.rs +++ b/apps/desktop/desktop_native/core/src/process_isolation/macos.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Result}; +use tracing::info; pub fn disable_coredumps() -> Result<()> { bail!("Not implemented on Mac") @@ -10,10 +11,7 @@ pub fn is_core_dumping_disabled() -> Result { pub fn isolate_process() -> Result<()> { let pid: u32 = std::process::id(); - println!( - "[Process Isolation] Disabling ptrace on main process ({}) via PT_DENY_ATTACH", - pid - ); + info!(pid, "Disabling ptrace on main process via PT_DENY_ATTACH."); secmem_proc::harden_process().map_err(|e| { anyhow::anyhow!( diff --git a/apps/desktop/desktop_native/core/src/process_isolation/windows.rs b/apps/desktop/desktop_native/core/src/process_isolation/windows.rs index dc1092f9131..fddea8bc53a 100644 --- a/apps/desktop/desktop_native/core/src/process_isolation/windows.rs +++ b/apps/desktop/desktop_native/core/src/process_isolation/windows.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Result}; +use tracing::info; pub fn disable_coredumps() -> Result<()> { bail!("Not implemented on Windows") @@ -10,10 +11,7 @@ pub fn is_core_dumping_disabled() -> Result { pub fn isolate_process() -> Result<()> { let pid: u32 = std::process::id(); - println!( - "[Process Isolation] Isolating main process via DACL {}", - pid - ); + info!(pid, "Isolating main process via DACL."); secmem_proc::harden_process().map_err(|e| { anyhow::anyhow!( From 5281da8fad6edc6901d22090f37fc8070bad0e11 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 17 Oct 2025 09:25:49 +0200 Subject: [PATCH 5/5] [PM-25660] UserKeyDefinition.clearOn doesn't clear data in some cases (#16799) * fix: always try to register clearOn events `registerEvents` already checks for existing registered events so there is no need to have a pre-check in `doStorageSave`. It causes issues because the `newState` and `oldState` parameters come from the custom deserializer which might never return `null` (e.g. transforming `null` to some default value). Better to just use the list of registered events as a source of truth. A performance check shows that most calls would only save a couple of milliseconds (ranges from 0.8 ms to 18 ms) and the total amount of time saved from application startup, to unlock, to showing the vault is about 100 ms. I haven't been able to perceive the change. * Revert "feat: add folder.clear warning (#16376)" This reverts commit a2e36c44890a21bcc290433ec240d0c225d9a7d1. --- apps/browser/src/background/main.background.ts | 1 - apps/cli/src/service-container/service-container.ts | 1 - apps/desktop/src/app/app.component.ts | 1 - apps/web/src/app/app.component.ts | 1 - .../vault-timeout/services/vault-timeout.service.ts | 1 - libs/state-internal/src/default-single-user-state.ts | 4 +--- 6 files changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3ccd78db18c..7181287aaae 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1674,7 +1674,6 @@ export default class MainBackground { await Promise.all([ this.keyService.clearKeys(userBeingLoggedOut), this.cipherService.clear(userBeingLoggedOut), - // ! DO NOT REMOVE folderService.clear ! For more information see PM-25660 this.folderService.clear(userBeingLoggedOut), this.vaultTimeoutSettingsService.clear(userBeingLoggedOut), this.biometricStateService.logout(userBeingLoggedOut), diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index ccce00fabd8..5e4de8d8564 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -949,7 +949,6 @@ export class ServiceContainer { this.eventUploadService.uploadEvents(userId as UserId), this.keyService.clearKeys(userId), this.cipherService.clear(userId), - // ! DO NOT REMOVE folderService.clear ! For more information see PM-25660 this.folderService.clear(userId), ]); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 1c2d3aa464d..683dcdd48c4 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -691,7 +691,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.keyService.clearKeys(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut); - // ! DO NOT REMOVE folderService.clear ! For more information see PM-25660 await this.folderService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); await this.biometricStateService.logout(userBeingLoggedOut); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 2fc81fe2119..60911173308 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -258,7 +258,6 @@ export class AppComponent implements OnDestroy, OnInit { await Promise.all([ this.keyService.clearKeys(userId), this.cipherService.clear(userId), - // ! DO NOT REMOVE folderService.clear ! For more information see PM-25660 this.folderService.clear(userId), this.biometricStateService.logout(userId), ]); diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts index 9b1350fc3a7..8b523498c31 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts @@ -144,7 +144,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.searchService.clearIndex(lockingUserId); - // ! DO NOT REMOVE folderService.clearDecryptedFolderState ! For more information see PM-25660 await this.folderService.clearDecryptedFolderState(lockingUserId); await this.masterPasswordService.clearMasterKey(lockingUserId); diff --git a/libs/state-internal/src/default-single-user-state.ts b/libs/state-internal/src/default-single-user-state.ts index 1496a710537..9155a221d56 100644 --- a/libs/state-internal/src/default-single-user-state.ts +++ b/libs/state-internal/src/default-single-user-state.ts @@ -31,8 +31,6 @@ export class DefaultSingleUserState protected override async doStorageSave(newState: T, oldState: T): Promise { await super.doStorageSave(newState, oldState); - if (newState != null && oldState == null) { - await this.stateEventRegistrarService.registerEvents(this.keyDefinition); - } + await this.stateEventRegistrarService.registerEvents(this.keyDefinition); } }