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:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,10 +110,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ import { KdfConfig } from "../models/kdf-config";
|
||||
|
||||
export abstract class KdfConfigService {
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,14 +37,14 @@ export class DefaultKdfConfigService implements KdfConfigService {
|
||||
await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId);
|
||||
}
|
||||
|
||||
async getKdfConfig(): Promise<KdfConfig> {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
async getKdfConfig(userId: UserId): Promise<KdfConfig> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
protected async buildPasswordExport(
|
||||
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 key = await this.pinService.makePinKey(password, salt, kdfConfig);
|
||||
|
||||
@@ -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<ExportedVault> {
|
||||
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<ExportedVaultAsString> {
|
||||
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<ExportedVaultAsBlob> {
|
||||
async getDecryptedExportZip(activeUserId: UserId): Promise<ExportedVaultAsBlob> {
|
||||
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<ExportedVaultAsString> {
|
||||
private async getDecryptedExport(
|
||||
activeUserId: UserId,
|
||||
format: "json" | "csv",
|
||||
): Promise<ExportedVaultAsString> {
|
||||
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<ExportedVaultAsString> {
|
||||
private async getEncryptedExport(activeUserId: UserId): Promise<ExportedVaultAsString> {
|
||||
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 = {
|
||||
|
||||
@@ -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<ExportedVaultAsString> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
@@ -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<string> {
|
||||
private async getEncryptedManagedExport(
|
||||
activeUserId: UserId,
|
||||
organizationId: string,
|
||||
): Promise<string> {
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user