From d43e4757dfcb3f7851c9c8e6bca180302d382315 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:25:27 -0500 Subject: [PATCH] [PM-7604] Require target UserID for KdfConfigService (#14380) * Require userId for KdfConfigService * Update auth team callers * Update tools team callers --- apps/cli/src/auth/commands/login.command.ts | 28 +-- .../settings/change-password.component.ts | 13 +- .../change-kdf/change-kdf.component.ts | 8 +- .../components/change-password.component.ts | 7 +- .../components/update-password.component.ts | 6 +- .../update-temp-password.component.ts | 7 +- .../pin/pin.service.implementation.ts | 4 +- .../user-verification.service.ts | 4 +- .../src/abstractions/kdf-config.service.ts | 2 +- .../src/kdf-config.service.spec.ts | 162 +++++++++--------- libs/key-management/src/kdf-config.service.ts | 8 +- .../src/services/base-vault-export.service.ts | 9 +- .../individual-vault-export.service.ts | 33 ++-- .../src/services/org-vault-export.service.ts | 22 ++- 14 files changed, 171 insertions(+), 142 deletions(-) diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 8d66a566038..8a94cc4175a 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -5,7 +5,7 @@ import * as http from "http"; import { OptionValues } from "commander"; import * as inquirer from "inquirer"; import Separator from "inquirer/lib/objects/separator"; -import { firstValueFrom, map, switchMap } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { LoginStrategyServiceAbstraction, @@ -29,7 +29,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; @@ -40,6 +39,7 @@ 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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; @@ -367,9 +367,9 @@ export class LoginCommand { clientSecret == null ) { if (response.forcePasswordReset === ForceSetPasswordReason.AdminForcePasswordReset) { - return await this.updateTempPassword(); + return await this.updateTempPassword(response.userId); } else if (response.forcePasswordReset === ForceSetPasswordReason.WeakMasterPassword) { - return await this.updateWeakPassword(password); + return await this.updateWeakPassword(response.userId, password); } } @@ -431,7 +431,7 @@ export class LoginCommand { return Response.success(res); } - private async updateWeakPassword(currentPassword: string) { + private async updateWeakPassword(userId: UserId, currentPassword: string) { // If no interaction available, alert user to use web vault if (!this.canInteract) { await this.logoutCallback(); @@ -448,6 +448,7 @@ export class LoginCommand { try { const { newPasswordHash, newUserKey, hint } = await this.collectNewMasterPasswordDetails( + userId, "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now.", ); @@ -469,7 +470,7 @@ export class LoginCommand { } } - private async updateTempPassword() { + private async updateTempPassword(userId: UserId) { // If no interaction available, alert user to use web vault if (!this.canInteract) { await this.logoutCallback(); @@ -486,6 +487,7 @@ export class LoginCommand { try { const { newPasswordHash, newUserKey, hint } = await this.collectNewMasterPasswordDetails( + userId, "An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.", ); @@ -510,10 +512,12 @@ export class LoginCommand { * Collect new master password and hint from the CLI. The collected password * is validated against any applicable master password policies, a new master * key is generated, and we use it to re-encrypt the user key + * @param userId - User ID of the account * @param prompt - Message that is displayed during the initial prompt * @param error */ private async collectNewMasterPasswordDetails( + userId: UserId, prompt: string, error?: string, ): Promise<{ @@ -539,11 +543,12 @@ export class LoginCommand { // Master Password Validation if (masterPassword == null || masterPassword === "") { - return this.collectNewMasterPasswordDetails(prompt, "Master password is required.\n"); + return this.collectNewMasterPasswordDetails(userId, prompt, "Master password is required.\n"); } if (masterPassword.length < Utils.minimumPasswordLength) { return this.collectNewMasterPasswordDetails( + userId, prompt, `Master password must be at least ${Utils.minimumPasswordLength} characters long.\n`, ); @@ -556,10 +561,7 @@ export class LoginCommand { ); const enforcedPolicyOptions = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), - ), + this.policyService.masterPasswordPolicyOptions$(userId), ); // Verify master password meets policy requirements @@ -572,6 +574,7 @@ export class LoginCommand { ) ) { return this.collectNewMasterPasswordDetails( + userId, prompt, "Your new master password does not meet the policy requirements.\n", ); @@ -589,6 +592,7 @@ export class LoginCommand { // Re-type Validation if (masterPassword !== masterPasswordRetype) { return this.collectNewMasterPasswordDetails( + userId, prompt, "Master password confirmation does not match.\n", ); @@ -601,7 +605,7 @@ export class LoginCommand { message: "Master Password Hint (optional):", }); const masterPasswordHint = hint.input; - const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(userId); // Create new key and hash new password const newMasterKey = await this.keyService.makeMasterKey( diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index d8e371fd36b..ffa5247ad08 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -310,13 +310,16 @@ export class ChangePasswordComponent newMasterKey: MasterKey, newUserKey: [UserKey, EncString], ) { - const masterKey = await this.keyService.makeMasterKey( - this.currentMasterPassword, - await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))), - await this.kdfConfigService.getKdfConfig(), + const [userId, email] = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), + ); + + const masterKey = await this.keyService.makeMasterKey( + this.currentMasterPassword, + email, + await this.kdfConfigService.getKdfConfig(userId), ); - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const newLocalKeyHash = await this.keyService.hashMasterKey( this.masterPassword, newMasterKey, diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index 3c392795ef4..cbbef0e016b 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DialogService } from "@bitwarden/components"; import { KdfConfigService, @@ -43,6 +45,7 @@ export class ChangeKdfComponent implements OnInit, OnDestroy { constructor( private dialogService: DialogService, private kdfConfigService: KdfConfigService, + private accountService: AccountService, private formBuilder: FormBuilder, ) { this.kdfOptions = [ @@ -52,7 +55,8 @@ export class ChangeKdfComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); this.formGroup.get("kdf").setValue(this.kdfConfig.kdfType); this.setFormControlValues(this.kdfConfig); diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 3b186a7fd2e..ca81f741b23 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -83,11 +83,12 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { return; } - const email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), + const [userId, email] = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), ); + if (this.kdfConfig == null) { - this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); } // Create new master key diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 77e854753d7..47affbecdf2 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { Directive } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -10,6 +11,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -96,8 +98,8 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { }); return false; } - - this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); return true; } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 267beb2b822..db2f319998a 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -110,10 +110,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp } async setupSubmitActions(): Promise { - this.email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), + const [userId, email] = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), ); - this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + this.email = email; + this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); return true; } diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index c0034020de8..4e363063f2f 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -172,7 +172,7 @@ export class PinService implements PinServiceAbstraction { const email = await firstValueFrom( this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)), ); - const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(userId); const pinKey = await this.makePinKey(pin, email, kdfConfig); return await this.encryptService.wrapSymmetricKey(userKey, pinKey); @@ -293,7 +293,7 @@ export class PinService implements PinServiceAbstraction { const email = await firstValueFrom( this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)), ); - const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(userId); const userKey: UserKey = await this.decryptUserKey( userId, diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 1ff629114ab..cfa6800deed 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -117,7 +117,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.keyService.makeMasterKey( verification.secret, email, - await this.kdfConfigService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(userId), ); } request.masterPasswordHash = alreadyHashed @@ -186,7 +186,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti throw new Error("Email is required. Cannot verify user by master password."); } - const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(userId); if (!kdfConfig) { throw new Error("KDF config is required. Cannot verify user by master password."); } diff --git a/libs/key-management/src/abstractions/kdf-config.service.ts b/libs/key-management/src/abstractions/kdf-config.service.ts index 9cc39561aa8..c6c4e5d4fb0 100644 --- a/libs/key-management/src/abstractions/kdf-config.service.ts +++ b/libs/key-management/src/abstractions/kdf-config.service.ts @@ -6,6 +6,6 @@ import { KdfConfig } from "../models/kdf-config"; export abstract class KdfConfigService { abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise; - abstract getKdfConfig(): Promise; + abstract getKdfConfig(userId: UserId): Promise; abstract getKdfConfig$(userId: UserId): Observable; } diff --git a/libs/key-management/src/kdf-config.service.spec.ts b/libs/key-management/src/kdf-config.service.spec.ts index 986d7abac40..97684266f5d 100644 --- a/libs/key-management/src/kdf-config.service.spec.ts +++ b/libs/key-management/src/kdf-config.service.spec.ts @@ -26,90 +26,94 @@ describe("KdfConfigService", () => { sutKdfConfigService = new DefaultKdfConfigService(fakeStateProvider); }); - it("setKdfConfig(): should set the PBKDF2KdfConfig config", async () => { - const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); - await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); - expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( - KDF_CONFIG, - kdfConfig, - mockUserId, - ); + describe("setKdfConfig", () => { + it("sets the PBKDF2KdfConfig config", async () => { + const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( + KDF_CONFIG, + kdfConfig, + mockUserId, + ); + }); + + it("sets the Argon2KdfConfig config", async () => { + const kdfConfig: KdfConfig = new Argon2KdfConfig(2, 63, 3); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( + KDF_CONFIG, + kdfConfig, + mockUserId, + ); + }); + + it("throws error KDF cannot be null", async () => { + try { + await sutKdfConfigService.setKdfConfig(mockUserId, null as unknown as KdfConfig); + } catch (e) { + expect(e).toEqual(new Error("kdfConfig cannot be null")); + } + }); + + it("throws error userId cannot be null", async () => { + const kdfConfig: KdfConfig = new Argon2KdfConfig(3, 64, 4); + try { + await sutKdfConfigService.setKdfConfig(null as unknown as UserId, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } + }); }); - it("setKdfConfig(): should set the Argon2KdfConfig config", async () => { - const kdfConfig: KdfConfig = new Argon2KdfConfig(2, 63, 3); - await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); - expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( - KDF_CONFIG, - kdfConfig, - mockUserId, - ); + describe("getKdfConfig", () => { + it("throws error if userId is null", async () => { + await expect(sutKdfConfigService.getKdfConfig(null as unknown as UserId)).rejects.toThrow( + "userId cannot be null", + ); + }); + + it("throws if target user doesn't have a KkfConfig", async () => { + const errorMessage = "KdfConfig for user " + mockUserId + " is null"; + await expect(sutKdfConfigService.getKdfConfig(mockUserId)).rejects.toThrow(errorMessage); + }); + + it("returns KdfConfig of target user", async () => { + const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); + await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); + await expect(sutKdfConfigService.getKdfConfig(mockUserId)).resolves.toEqual(kdfConfig); + }); }); - it("setKdfConfig(): should throw error KDF cannot be null", async () => { - try { - await sutKdfConfigService.setKdfConfig(mockUserId, null as unknown as KdfConfig); - } catch (e) { - expect(e).toEqual(new Error("kdfConfig cannot be null")); - } - }); + describe("getKdfConfig$", () => { + it("gets KdfConfig of provided user", async () => { + await expect( + firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId)), + ).resolves.toBeNull(); + const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); + await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); + await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toEqual( + kdfConfig, + ); + }); - it("setKdfConfig(): should throw error userId cannot be null", async () => { - const kdfConfig: KdfConfig = new Argon2KdfConfig(3, 64, 4); - try { - await sutKdfConfigService.setKdfConfig(null as unknown as UserId, kdfConfig); - } catch (e) { - expect(e).toEqual(new Error("userId cannot be null")); - } - }); + it("gets KdfConfig of provided user after changed", async () => { + await expect( + firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId)), + ).resolves.toBeNull(); + await fakeStateProvider.setUserState(KDF_CONFIG, new PBKDF2KdfConfig(500_000), mockUserId); + const kdfConfigChanged: KdfConfig = new PBKDF2KdfConfig(500_001); + await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfigChanged, mockUserId); + await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toEqual( + kdfConfigChanged, + ); + }); - it("getKdfConfig(): should get KdfConfig of active user", async () => { - const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); - await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); - await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); - }); - - it("getKdfConfig(): should throw error KdfConfig can only be retrieved when there is active user", async () => { - fakeAccountService.activeAccountSubject.next(null); - try { - await sutKdfConfigService.getKdfConfig(); - } catch (e) { - expect(e).toEqual(new Error("KdfConfig can only be retrieved when there is active user")); - } - }); - - it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { - try { - await sutKdfConfigService.getKdfConfig(); - } catch (e) { - expect(e).toEqual(new Error("KdfConfig for active user account state is null")); - } - }); - - it("getKdfConfig$(UserId): should get KdfConfig of provided user", async () => { - await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toBeNull(); - const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); - await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); - await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toEqual( - kdfConfig, - ); - }); - - it("getKdfConfig$(UserId): should get KdfConfig of provided user after changed", async () => { - await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toBeNull(); - await fakeStateProvider.setUserState(KDF_CONFIG, new PBKDF2KdfConfig(500_000), mockUserId); - const kdfConfigChanged: KdfConfig = new PBKDF2KdfConfig(500_001); - await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfigChanged, mockUserId); - await expect(firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId))).resolves.toEqual( - kdfConfigChanged, - ); - }); - - it("getKdfConfig$(UserId): should throw error userId cannot be null", async () => { - try { - sutKdfConfigService.getKdfConfig$(null as unknown as UserId); - } catch (e) { - expect(e).toEqual(new Error("userId cannot be null")); - } + it("throws error userId cannot be null", async () => { + try { + sutKdfConfigService.getKdfConfig$(null as unknown as UserId); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } + }); }); }); diff --git a/libs/key-management/src/kdf-config.service.ts b/libs/key-management/src/kdf-config.service.ts index efc5310e5a8..24635e87580 100644 --- a/libs/key-management/src/kdf-config.service.ts +++ b/libs/key-management/src/kdf-config.service.ts @@ -37,14 +37,14 @@ export class DefaultKdfConfigService implements KdfConfigService { await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); } - async getKdfConfig(): Promise { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); + async getKdfConfig(userId: UserId): Promise { if (userId == null) { - throw new Error("KdfConfig can only be retrieved when there is active user"); + throw new Error("userId cannot be null"); } + const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$); if (state == null) { - throw new Error("KdfConfig for active user account state is null"); + throw new Error("KdfConfig for user " + userId + " is null"); } return state; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts index 0a92f4f02d7..c1526ba0f4b 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts @@ -4,6 +4,7 @@ import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"; @@ -17,8 +18,12 @@ export class BaseVaultExportService { private kdfConfigService: KdfConfigService, ) {} - protected async buildPasswordExport(clearText: string, password: string): Promise { - const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); + protected async buildPasswordExport( + userId: UserId, + clearText: string, + password: string, + ): Promise { + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(userId); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); const key = await this.pinService.makePinKey(password, salt, kdfConfig); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index d253ae8d0b1..96b19acd963 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -13,6 +13,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -59,19 +60,21 @@ export class IndividualVaultExportService * @param format The format of the export */ async getExport(format: ExportFormat = "csv"): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (format === "encrypted_json") { - return this.getEncryptedExport(); + return this.getEncryptedExport(userId); } else if (format === "zip") { - return this.getDecryptedExportZip(); + return this.getDecryptedExportZip(userId); } - return this.getDecryptedExport(format); + return this.getDecryptedExport(userId, format); } - /** Creates a password protected export of an individiual vault (My Vault) as a JSON file + /** Creates a password protected export of an individual vault (My Vault) as a JSON file * @param password The password to encrypt the export with * @returns A password-protected encrypted individual vault export */ async getPasswordProtectedExport(password: string): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const exportVault = await this.getExport("json"); if (exportVault.type !== "text/plain") { @@ -80,19 +83,20 @@ export class IndividualVaultExportService return { type: "text/plain", - data: await this.buildPasswordExport(exportVault.data, password), + data: await this.buildPasswordExport(userId, exportVault.data, password), fileName: ExportHelper.getFileName("", "encrypted_json"), } as ExportedVaultAsString; } /** Creates a unencrypted export of an individual vault including attachments + * @param activeUserId The user ID of the user requesting the export * @returns A unencrypted export including attachments */ - async getDecryptedExportZip(): Promise { + async getDecryptedExportZip(activeUserId: UserId): Promise { const zip = new JSZip(); // ciphers - const exportedVault = await this.getDecryptedExport("json"); + const exportedVault = await this.getDecryptedExport(activeUserId, "json"); zip.file("data.json", exportedVault.data); const attachmentsFolder = zip.folder("attachments"); @@ -100,8 +104,6 @@ export class IndividualVaultExportService throw new Error("Error creating attachments folder"); } - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - // attachments for (const cipher of await this.cipherService.getAllDecrypted(activeUserId)) { if ( @@ -161,11 +163,13 @@ export class IndividualVaultExportService } } - private async getDecryptedExport(format: "json" | "csv"): Promise { + private async getDecryptedExport( + activeUserId: UserId, + format: "json" | "csv", + ): Promise { let decFolders: FolderView[] = []; let decCiphers: CipherView[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => { @@ -196,11 +200,10 @@ export class IndividualVaultExportService } as ExportedVaultAsString; } - private async getEncryptedExport(): Promise { + private async getEncryptedExport(activeUserId: UserId): Promise { let folders: Folder[] = []; let ciphers: Cipher[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => { @@ -216,9 +219,7 @@ export class IndividualVaultExportService await Promise.all(promises); - const userKey = await this.keyService.getUserKeyWithLegacySupport( - await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), - ); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey); const jsonDoc: BitwardenEncryptedIndividualJsonExport = { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index e4ed105d1ad..86edf67bf03 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -18,7 +18,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; @@ -67,6 +67,7 @@ export class OrganizationVaultExportService password: string, onlyManagedCollections: boolean, ): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const exportVault = await this.getOrganizationExport( organizationId, "json", @@ -75,7 +76,7 @@ export class OrganizationVaultExportService return { type: "text/plain", - data: await this.buildPasswordExport(exportVault.data, password), + data: await this.buildPasswordExport(userId, exportVault.data, password), fileName: ExportHelper.getFileName("org", "encrypted_json"), } as ExportedVaultAsString; } @@ -102,12 +103,13 @@ export class OrganizationVaultExportService if (format === "zip") { throw new Error("Zip export not supported for organization"); } + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (format === "encrypted_json") { return { type: "text/plain", data: onlyManagedCollections - ? await this.getEncryptedManagedExport(organizationId) + ? await this.getEncryptedManagedExport(userId, organizationId) : await this.getOrganizationEncryptedExport(organizationId), fileName: ExportHelper.getFileName("org", "encrypted_json"), } as ExportedVaultAsString; @@ -116,20 +118,20 @@ export class OrganizationVaultExportService return { type: "text/plain", data: onlyManagedCollections - ? await this.getDecryptedManagedExport(organizationId, format) - : await this.getOrganizationDecryptedExport(organizationId, format), + ? await this.getDecryptedManagedExport(userId, organizationId, format) + : await this.getOrganizationDecryptedExport(userId, organizationId, format), fileName: ExportHelper.getFileName("org", format), } as ExportedVaultAsString; } private async getOrganizationDecryptedExport( + activeUserId: UserId, organizationId: string, format: "json" | "csv", ): Promise { const decCollections: CollectionView[] = []; const decCiphers: CipherView[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( this.apiService.getOrganizationExport(organizationId).then((exportData) => { @@ -210,6 +212,7 @@ export class OrganizationVaultExportService } private async getDecryptedManagedExport( + activeUserId: UserId, organizationId: string, format: "json" | "csv", ): Promise { @@ -217,7 +220,6 @@ export class OrganizationVaultExportService let allDecCiphers: CipherView[] = []; let decCollections: CollectionView[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( this.collectionService.getAllDecrypted().then(async (collections) => { @@ -245,12 +247,14 @@ export class OrganizationVaultExportService return this.buildJsonExport(decCollections, decCiphers); } - private async getEncryptedManagedExport(organizationId: string): Promise { + private async getEncryptedManagedExport( + activeUserId: UserId, + organizationId: string, + ): Promise { let encCiphers: Cipher[] = []; let allCiphers: Cipher[] = []; let encCollections: Collection[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( this.collectionService.getAll().then((collections) => {