1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-7604] Require target UserID for KdfConfigService (#14380)

* Require userId for KdfConfigService

* Update auth team callers

* Update tools team callers
This commit is contained in:
Thomas Avery
2025-04-29 17:25:27 -05:00
committed by GitHub
parent f39e37002b
commit d43e4757df
14 changed files with 171 additions and 142 deletions

View File

@@ -5,7 +5,7 @@ import * as http from "http";
import { OptionValues } from "commander"; import { OptionValues } from "commander";
import * as inquirer from "inquirer"; import * as inquirer from "inquirer";
import Separator from "inquirer/lib/objects/separator"; import Separator from "inquirer/lib/objects/separator";
import { firstValueFrom, map, switchMap } from "rxjs"; import { firstValueFrom, map } from "rxjs";
import { import {
LoginStrategyServiceAbstraction, 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 { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.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 { 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 { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; 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"; 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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { KdfConfigService, KeyService } from "@bitwarden/key-management";
@@ -367,9 +367,9 @@ export class LoginCommand {
clientSecret == null clientSecret == null
) { ) {
if (response.forcePasswordReset === ForceSetPasswordReason.AdminForcePasswordReset) { if (response.forcePasswordReset === ForceSetPasswordReason.AdminForcePasswordReset) {
return await this.updateTempPassword(); return await this.updateTempPassword(response.userId);
} else if (response.forcePasswordReset === ForceSetPasswordReason.WeakMasterPassword) { } 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); 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 no interaction available, alert user to use web vault
if (!this.canInteract) { if (!this.canInteract) {
await this.logoutCallback(); await this.logoutCallback();
@@ -448,6 +448,7 @@ export class LoginCommand {
try { try {
const { newPasswordHash, newUserKey, hint } = await this.collectNewMasterPasswordDetails( 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.", "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 no interaction available, alert user to use web vault
if (!this.canInteract) { if (!this.canInteract) {
await this.logoutCallback(); await this.logoutCallback();
@@ -486,6 +487,7 @@ export class LoginCommand {
try { try {
const { newPasswordHash, newUserKey, hint } = await this.collectNewMasterPasswordDetails( 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.", "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 * Collect new master password and hint from the CLI. The collected password
* is validated against any applicable master password policies, a new master * is validated against any applicable master password policies, a new master
* key is generated, and we use it to re-encrypt the user key * 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 prompt - Message that is displayed during the initial prompt
* @param error * @param error
*/ */
private async collectNewMasterPasswordDetails( private async collectNewMasterPasswordDetails(
userId: UserId,
prompt: string, prompt: string,
error?: string, error?: string,
): Promise<{ ): Promise<{
@@ -539,11 +543,12 @@ export class LoginCommand {
// Master Password Validation // Master Password Validation
if (masterPassword == null || masterPassword === "") { 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) { if (masterPassword.length < Utils.minimumPasswordLength) {
return this.collectNewMasterPasswordDetails( return this.collectNewMasterPasswordDetails(
userId,
prompt, prompt,
`Master password must be at least ${Utils.minimumPasswordLength} characters long.\n`, `Master password must be at least ${Utils.minimumPasswordLength} characters long.\n`,
); );
@@ -556,10 +561,7 @@ export class LoginCommand {
); );
const enforcedPolicyOptions = await firstValueFrom( const enforcedPolicyOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe( this.policyService.masterPasswordPolicyOptions$(userId),
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
),
); );
// Verify master password meets policy requirements // Verify master password meets policy requirements
@@ -572,6 +574,7 @@ export class LoginCommand {
) )
) { ) {
return this.collectNewMasterPasswordDetails( return this.collectNewMasterPasswordDetails(
userId,
prompt, prompt,
"Your new master password does not meet the policy requirements.\n", "Your new master password does not meet the policy requirements.\n",
); );
@@ -589,6 +592,7 @@ export class LoginCommand {
// Re-type Validation // Re-type Validation
if (masterPassword !== masterPasswordRetype) { if (masterPassword !== masterPasswordRetype) {
return this.collectNewMasterPasswordDetails( return this.collectNewMasterPasswordDetails(
userId,
prompt, prompt,
"Master password confirmation does not match.\n", "Master password confirmation does not match.\n",
); );
@@ -601,7 +605,7 @@ export class LoginCommand {
message: "Master Password Hint (optional):", message: "Master Password Hint (optional):",
}); });
const masterPasswordHint = hint.input; const masterPasswordHint = hint.input;
const kdfConfig = await this.kdfConfigService.getKdfConfig(); const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
// Create new key and hash new password // Create new key and hash new password
const newMasterKey = await this.keyService.makeMasterKey( const newMasterKey = await this.keyService.makeMasterKey(

View File

@@ -310,13 +310,16 @@ export class ChangePasswordComponent
newMasterKey: MasterKey, newMasterKey: MasterKey,
newUserKey: [UserKey, EncString], newUserKey: [UserKey, EncString],
) { ) {
const masterKey = await this.keyService.makeMasterKey( const [userId, email] = await firstValueFrom(
this.currentMasterPassword, this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))), );
await this.kdfConfigService.getKdfConfig(),
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( const newLocalKeyHash = await this.keyService.hashMasterKey(
this.masterPassword, this.masterPassword,
newMasterKey, newMasterKey,

View File

@@ -2,8 +2,10 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms"; 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 { DialogService } from "@bitwarden/components";
import { import {
KdfConfigService, KdfConfigService,
@@ -43,6 +45,7 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
constructor( constructor(
private dialogService: DialogService, private dialogService: DialogService,
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
private accountService: AccountService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
) { ) {
this.kdfOptions = [ this.kdfOptions = [
@@ -52,7 +55,8 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
} }
async ngOnInit() { 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.formGroup.get("kdf").setValue(this.kdfConfig.kdfType);
this.setFormControlValues(this.kdfConfig); this.setFormControlValues(this.kdfConfig);

View File

@@ -83,11 +83,12 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
return; return;
} }
const email = await firstValueFrom( const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)), this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
); );
if (this.kdfConfig == null) { if (this.kdfConfig == null) {
this.kdfConfig = await this.kdfConfigService.getKdfConfig(); this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
} }
// Create new master key // Create new master key

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Directive } from "@angular/core"; import { Directive } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; 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"; 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; 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 { Verification } from "@bitwarden/common/auth/types/verification";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -96,8 +98,8 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
}); });
return false; return false;
} }
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.kdfConfig = await this.kdfConfigService.getKdfConfig(); this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
return true; return true;
} }

View File

@@ -110,10 +110,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
} }
async setupSubmitActions(): Promise<boolean> { async setupSubmitActions(): Promise<boolean> {
this.email = await firstValueFrom( const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)), 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; return true;
} }

View File

@@ -172,7 +172,7 @@ export class PinService implements PinServiceAbstraction {
const email = await firstValueFrom( const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)), 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); const pinKey = await this.makePinKey(pin, email, kdfConfig);
return await this.encryptService.wrapSymmetricKey(userKey, pinKey); return await this.encryptService.wrapSymmetricKey(userKey, pinKey);
@@ -293,7 +293,7 @@ export class PinService implements PinServiceAbstraction {
const email = await firstValueFrom( const email = await firstValueFrom(
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)), 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( const userKey: UserKey = await this.decryptUserKey(
userId, userId,

View File

@@ -117,7 +117,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
masterKey = await this.keyService.makeMasterKey( masterKey = await this.keyService.makeMasterKey(
verification.secret, verification.secret,
email, email,
await this.kdfConfigService.getKdfConfig(), await this.kdfConfigService.getKdfConfig(userId),
); );
} }
request.masterPasswordHash = alreadyHashed request.masterPasswordHash = alreadyHashed
@@ -186,7 +186,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
throw new Error("Email is required. Cannot verify user by master password."); 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) { if (!kdfConfig) {
throw new Error("KDF config is required. Cannot verify user by master password."); throw new Error("KDF config is required. Cannot verify user by master password.");
} }

View File

@@ -6,6 +6,6 @@ import { KdfConfig } from "../models/kdf-config";
export abstract class KdfConfigService { export abstract class KdfConfigService {
abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise<void>; abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise<void>;
abstract getKdfConfig(): Promise<KdfConfig>; abstract getKdfConfig(userId: UserId): Promise<KdfConfig>;
abstract getKdfConfig$(userId: UserId): Observable<KdfConfig | null>; abstract getKdfConfig$(userId: UserId): Observable<KdfConfig | null>;
} }

View File

@@ -26,90 +26,94 @@ describe("KdfConfigService", () => {
sutKdfConfigService = new DefaultKdfConfigService(fakeStateProvider); sutKdfConfigService = new DefaultKdfConfigService(fakeStateProvider);
}); });
it("setKdfConfig(): should set the PBKDF2KdfConfig config", async () => { describe("setKdfConfig", () => {
const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); it("sets the PBKDF2KdfConfig config", async () => {
await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000);
expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig);
KDF_CONFIG, expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith(
kdfConfig, KDF_CONFIG,
mockUserId, 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 () => { describe("getKdfConfig", () => {
const kdfConfig: KdfConfig = new Argon2KdfConfig(2, 63, 3); it("throws error if userId is null", async () => {
await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); await expect(sutKdfConfigService.getKdfConfig(null as unknown as UserId)).rejects.toThrow(
expect(fakeStateProvider.mock.setUserState).toHaveBeenCalledWith( "userId cannot be null",
KDF_CONFIG, );
kdfConfig, });
mockUserId,
); 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 () => { describe("getKdfConfig$", () => {
try { it("gets KdfConfig of provided user", async () => {
await sutKdfConfigService.setKdfConfig(mockUserId, null as unknown as KdfConfig); await expect(
} catch (e) { firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId)),
expect(e).toEqual(new Error("kdfConfig cannot be null")); ).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 () => { it("gets KdfConfig of provided user after changed", async () => {
const kdfConfig: KdfConfig = new Argon2KdfConfig(3, 64, 4); await expect(
try { firstValueFrom(sutKdfConfigService.getKdfConfig$(mockUserId)),
await sutKdfConfigService.setKdfConfig(null as unknown as UserId, kdfConfig); ).resolves.toBeNull();
} catch (e) { await fakeStateProvider.setUserState(KDF_CONFIG, new PBKDF2KdfConfig(500_000), mockUserId);
expect(e).toEqual(new Error("userId cannot be null")); 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 () => { it("throws error userId cannot be null", async () => {
const kdfConfig: KdfConfig = new PBKDF2KdfConfig(500_000); try {
await fakeStateProvider.setUserState(KDF_CONFIG, kdfConfig, mockUserId); sutKdfConfigService.getKdfConfig$(null as unknown as UserId);
await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); } catch (e) {
}); expect(e).toEqual(new Error("userId cannot be null"));
}
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"));
}
}); });
}); });

View File

@@ -37,14 +37,14 @@ export class DefaultKdfConfigService implements KdfConfigService {
await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId);
} }
async getKdfConfig(): Promise<KdfConfig> { async getKdfConfig(userId: UserId): Promise<KdfConfig> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) { 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$); const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$);
if (state == null) { 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; return state;
} }

View File

@@ -4,6 +4,7 @@ import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management"; import { KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management";
@@ -17,8 +18,12 @@ export class BaseVaultExportService {
private kdfConfigService: KdfConfigService, private kdfConfigService: KdfConfigService,
) {} ) {}
protected async buildPasswordExport(clearText: string, password: string): Promise<string> { protected async buildPasswordExport(
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); userId: UserId,
clearText: string,
password: string,
): Promise<string> {
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(userId);
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16));
const key = await this.pinService.makePinKey(password, salt, kdfConfig); const key = await this.pinService.makePinKey(password, salt, kdfConfig);

View File

@@ -13,6 +13,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -59,19 +60,21 @@ export class IndividualVaultExportService
* @param format The format of the export * @param format The format of the export
*/ */
async getExport(format: ExportFormat = "csv"): Promise<ExportedVault> { async getExport(format: ExportFormat = "csv"): Promise<ExportedVault> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (format === "encrypted_json") { if (format === "encrypted_json") {
return this.getEncryptedExport(); return this.getEncryptedExport(userId);
} else if (format === "zip") { } 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 * @param password The password to encrypt the export with
* @returns A password-protected encrypted individual vault export * @returns A password-protected encrypted individual vault export
*/ */
async getPasswordProtectedExport(password: string): Promise<ExportedVaultAsString> { async getPasswordProtectedExport(password: string): Promise<ExportedVaultAsString> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const exportVault = await this.getExport("json"); const exportVault = await this.getExport("json");
if (exportVault.type !== "text/plain") { if (exportVault.type !== "text/plain") {
@@ -80,19 +83,20 @@ export class IndividualVaultExportService
return { return {
type: "text/plain", type: "text/plain",
data: await this.buildPasswordExport(exportVault.data, password), data: await this.buildPasswordExport(userId, exportVault.data, password),
fileName: ExportHelper.getFileName("", "encrypted_json"), fileName: ExportHelper.getFileName("", "encrypted_json"),
} as ExportedVaultAsString; } as ExportedVaultAsString;
} }
/** Creates a unencrypted export of an individual vault including attachments /** 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 * @returns A unencrypted export including attachments
*/ */
async getDecryptedExportZip(): Promise<ExportedVaultAsBlob> { async getDecryptedExportZip(activeUserId: UserId): Promise<ExportedVaultAsBlob> {
const zip = new JSZip(); const zip = new JSZip();
// ciphers // ciphers
const exportedVault = await this.getDecryptedExport("json"); const exportedVault = await this.getDecryptedExport(activeUserId, "json");
zip.file("data.json", exportedVault.data); zip.file("data.json", exportedVault.data);
const attachmentsFolder = zip.folder("attachments"); const attachmentsFolder = zip.folder("attachments");
@@ -100,8 +104,6 @@ export class IndividualVaultExportService
throw new Error("Error creating attachments folder"); throw new Error("Error creating attachments folder");
} }
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
// attachments // attachments
for (const cipher of await this.cipherService.getAllDecrypted(activeUserId)) { for (const cipher of await this.cipherService.getAllDecrypted(activeUserId)) {
if ( if (
@@ -161,11 +163,13 @@ export class IndividualVaultExportService
} }
} }
private async getDecryptedExport(format: "json" | "csv"): Promise<ExportedVaultAsString> { private async getDecryptedExport(
activeUserId: UserId,
format: "json" | "csv",
): Promise<ExportedVaultAsString> {
let decFolders: FolderView[] = []; let decFolders: FolderView[] = [];
let decCiphers: CipherView[] = []; let decCiphers: CipherView[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => { firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => {
@@ -196,11 +200,10 @@ export class IndividualVaultExportService
} as ExportedVaultAsString; } as ExportedVaultAsString;
} }
private async getEncryptedExport(): Promise<ExportedVaultAsString> { private async getEncryptedExport(activeUserId: UserId): Promise<ExportedVaultAsString> {
let folders: Folder[] = []; let folders: Folder[] = [];
let ciphers: Cipher[] = []; let ciphers: Cipher[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => { firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => {
@@ -216,9 +219,7 @@ export class IndividualVaultExportService
await Promise.all(promises); await Promise.all(promises);
const userKey = await this.keyService.getUserKeyWithLegacySupport( const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
);
const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey); const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey);
const jsonDoc: BitwardenEncryptedIndividualJsonExport = { const jsonDoc: BitwardenEncryptedIndividualJsonExport = {

View File

@@ -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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
@@ -67,6 +67,7 @@ export class OrganizationVaultExportService
password: string, password: string,
onlyManagedCollections: boolean, onlyManagedCollections: boolean,
): Promise<ExportedVaultAsString> { ): Promise<ExportedVaultAsString> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const exportVault = await this.getOrganizationExport( const exportVault = await this.getOrganizationExport(
organizationId, organizationId,
"json", "json",
@@ -75,7 +76,7 @@ export class OrganizationVaultExportService
return { return {
type: "text/plain", type: "text/plain",
data: await this.buildPasswordExport(exportVault.data, password), data: await this.buildPasswordExport(userId, exportVault.data, password),
fileName: ExportHelper.getFileName("org", "encrypted_json"), fileName: ExportHelper.getFileName("org", "encrypted_json"),
} as ExportedVaultAsString; } as ExportedVaultAsString;
} }
@@ -102,12 +103,13 @@ export class OrganizationVaultExportService
if (format === "zip") { if (format === "zip") {
throw new Error("Zip export not supported for organization"); throw new Error("Zip export not supported for organization");
} }
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (format === "encrypted_json") { if (format === "encrypted_json") {
return { return {
type: "text/plain", type: "text/plain",
data: onlyManagedCollections data: onlyManagedCollections
? await this.getEncryptedManagedExport(organizationId) ? await this.getEncryptedManagedExport(userId, organizationId)
: await this.getOrganizationEncryptedExport(organizationId), : await this.getOrganizationEncryptedExport(organizationId),
fileName: ExportHelper.getFileName("org", "encrypted_json"), fileName: ExportHelper.getFileName("org", "encrypted_json"),
} as ExportedVaultAsString; } as ExportedVaultAsString;
@@ -116,20 +118,20 @@ export class OrganizationVaultExportService
return { return {
type: "text/plain", type: "text/plain",
data: onlyManagedCollections data: onlyManagedCollections
? await this.getDecryptedManagedExport(organizationId, format) ? await this.getDecryptedManagedExport(userId, organizationId, format)
: await this.getOrganizationDecryptedExport(organizationId, format), : await this.getOrganizationDecryptedExport(userId, organizationId, format),
fileName: ExportHelper.getFileName("org", format), fileName: ExportHelper.getFileName("org", format),
} as ExportedVaultAsString; } as ExportedVaultAsString;
} }
private async getOrganizationDecryptedExport( private async getOrganizationDecryptedExport(
activeUserId: UserId,
organizationId: string, organizationId: string,
format: "json" | "csv", format: "json" | "csv",
): Promise<string> { ): Promise<string> {
const decCollections: CollectionView[] = []; const decCollections: CollectionView[] = [];
const decCiphers: CipherView[] = []; const decCiphers: CipherView[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
this.apiService.getOrganizationExport(organizationId).then((exportData) => { this.apiService.getOrganizationExport(organizationId).then((exportData) => {
@@ -210,6 +212,7 @@ export class OrganizationVaultExportService
} }
private async getDecryptedManagedExport( private async getDecryptedManagedExport(
activeUserId: UserId,
organizationId: string, organizationId: string,
format: "json" | "csv", format: "json" | "csv",
): Promise<string> { ): Promise<string> {
@@ -217,7 +220,6 @@ export class OrganizationVaultExportService
let allDecCiphers: CipherView[] = []; let allDecCiphers: CipherView[] = [];
let decCollections: CollectionView[] = []; let decCollections: CollectionView[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
this.collectionService.getAllDecrypted().then(async (collections) => { this.collectionService.getAllDecrypted().then(async (collections) => {
@@ -245,12 +247,14 @@ export class OrganizationVaultExportService
return this.buildJsonExport(decCollections, decCiphers); return this.buildJsonExport(decCollections, decCiphers);
} }
private async getEncryptedManagedExport(organizationId: string): Promise<string> { private async getEncryptedManagedExport(
activeUserId: UserId,
organizationId: string,
): Promise<string> {
let encCiphers: Cipher[] = []; let encCiphers: Cipher[] = [];
let allCiphers: Cipher[] = []; let allCiphers: Cipher[] = [];
let encCollections: Collection[] = []; let encCollections: Collection[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
this.collectionService.getAll().then((collections) => { this.collectionService.getAll().then((collections) => {