mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-23230] Implement KDF Change Service (#15748)
* Add new mp service api * Fix tests * Add test coverage * Add newline * Fix type * Rename to "unwrapUserKeyFromMasterPasswordUnlockData" * Fix build * Fix build on cli * Fix linting * Re-sort spec * Add tests * Fix test and build issues * Fix build * Clean up * Remove introduced function * Clean up comments * Fix abstract class types * Fix comments * Cleanup * Cleanup * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/services/master-password.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add comments * Fix build * Add arg null check * Cleanup * Fix build * Fix build on browser * Implement KDF change service * Deprecate encryptUserKeyWithMasterKey * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for null params * Fix builds * Cleanup and deprecate more functions * Fix formatting * Prettier * Clean up * Update libs/key-management/src/abstractions/key.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Make emailToSalt private and expose abstract saltForUser * Add tests * Add docs * Fix build * Fix tests * Fix tests * Address feedback and fix primitive obsession * Consolidate active account checks in change kdf confirmation component * Update libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add defensive parameter checks * Add tests * Add comment for follow-up epic * Move change kdf service, remove abstraction and add api service * Fix test * Drop redundant null check * Address feedback * Add throw on empty password * Fix tests * Mark change kdf service as internal * Add abstract classes * Switch to abstraction * use sdk EncString in MasterPasswordUnlockData * fix remaining tests --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
@@ -127,8 +127,7 @@ describe("DesktopSetInitialPasswordService", () => {
|
|||||||
credentials.newPasswordHint,
|
credentials.newPasswordHint,
|
||||||
credentials.orgSsoIdentifier,
|
credentials.orgSsoIdentifier,
|
||||||
keysRequest,
|
keysRequest,
|
||||||
credentials.kdfConfig.kdfType,
|
credentials.kdfConfig,
|
||||||
credentials.kdfConfig.iterations,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -131,8 +131,7 @@ describe("WebSetInitialPasswordService", () => {
|
|||||||
credentials.newPasswordHint,
|
credentials.newPasswordHint,
|
||||||
credentials.orgSsoIdentifier,
|
credentials.orgSsoIdentifier,
|
||||||
keysRequest,
|
keysRequest,
|
||||||
credentials.kdfConfig.kdfType,
|
credentials.kdfConfig,
|
||||||
credentials.kdfConfig.iterations,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { Component, Inject } from "@angular/core";
|
|||||||
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
|
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||||
import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management";
|
import { KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-change-kdf-confirmation",
|
selector: "app-change-kdf-confirmation",
|
||||||
@@ -28,13 +28,12 @@ export class ChangeKdfConfirmationComponent {
|
|||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private keyService: KeyService,
|
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
|
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
|
private changeKdfService: ChangeKdfService,
|
||||||
) {
|
) {
|
||||||
this.kdfConfig = params.kdfConfig;
|
this.kdfConfig = params.kdfConfig;
|
||||||
this.masterPassword = null;
|
this.masterPassword = null;
|
||||||
@@ -56,37 +55,17 @@ export class ChangeKdfConfirmationComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async makeKeyAndSaveAsync() {
|
private async makeKeyAndSaveAsync() {
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
|
||||||
if (activeAccount == null) {
|
|
||||||
throw new Error("No active account found.");
|
|
||||||
}
|
|
||||||
const masterPassword = this.form.value.masterPassword;
|
const masterPassword = this.form.value.masterPassword;
|
||||||
|
|
||||||
// Ensure the KDF config is valid.
|
// Ensure the KDF config is valid.
|
||||||
this.kdfConfig.validateKdfConfigForSetting();
|
this.kdfConfig.validateKdfConfigForSetting();
|
||||||
|
|
||||||
const request = new KdfRequest();
|
const activeAccountId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||||
request.kdf = this.kdfConfig.kdfType;
|
|
||||||
request.kdfIterations = this.kdfConfig.iterations;
|
|
||||||
if (this.kdfConfig.kdfType === KdfType.Argon2id) {
|
|
||||||
request.kdfMemory = this.kdfConfig.memory;
|
|
||||||
request.kdfParallelism = this.kdfConfig.parallelism;
|
|
||||||
}
|
|
||||||
const masterKey = await this.keyService.getOrDeriveMasterKey(masterPassword, activeAccount.id);
|
|
||||||
request.masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
|
|
||||||
|
|
||||||
const newMasterKey = await this.keyService.makeMasterKey(
|
await this.changeKdfService.updateUserKdfParams(
|
||||||
masterPassword,
|
masterPassword,
|
||||||
activeAccount.email,
|
|
||||||
this.kdfConfig,
|
this.kdfConfig,
|
||||||
|
activeAccountId,
|
||||||
);
|
);
|
||||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
|
||||||
masterPassword,
|
|
||||||
newMasterKey,
|
|
||||||
);
|
|
||||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
|
||||||
request.key = newUserKey[1].encryptedString;
|
|
||||||
|
|
||||||
await this.apiService.postAccountKdf(request);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
@@ -7,6 +7,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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||||
|
import { firstValueFromOrThrow } from "@bitwarden/common/key-management/utils";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -103,18 +104,18 @@ export class UserKeyRotationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read current cryptographic state / settings
|
// Read current cryptographic state / settings
|
||||||
const masterKeyKdfConfig: KdfConfig = (await this.firstValueFromOrThrow(
|
const masterKeyKdfConfig: KdfConfig = (await firstValueFromOrThrow(
|
||||||
this.kdfConfigService.getKdfConfig$(user.id),
|
this.kdfConfigService.getKdfConfig$(user.id),
|
||||||
"KDF config",
|
"KDF config",
|
||||||
))!;
|
))!;
|
||||||
// The masterkey salt used for deriving the masterkey always needs to be trimmed and lowercased.
|
// The masterkey salt used for deriving the masterkey always needs to be trimmed and lowercased.
|
||||||
const masterKeySalt = user.email.trim().toLowerCase();
|
const masterKeySalt = user.email.trim().toLowerCase();
|
||||||
const currentUserKey: UserKey = (await this.firstValueFromOrThrow(
|
const currentUserKey: UserKey = (await firstValueFromOrThrow(
|
||||||
this.keyService.userKey$(user.id),
|
this.keyService.userKey$(user.id),
|
||||||
"User key",
|
"User key",
|
||||||
))!;
|
))!;
|
||||||
const currentUserKeyWrappedPrivateKey = new EncString(
|
const currentUserKeyWrappedPrivateKey = new EncString(
|
||||||
(await this.firstValueFromOrThrow(
|
(await firstValueFromOrThrow(
|
||||||
this.keyService.userEncryptedPrivateKey$(user.id),
|
this.keyService.userEncryptedPrivateKey$(user.id),
|
||||||
"User encrypted private key",
|
"User encrypted private key",
|
||||||
))!,
|
))!,
|
||||||
@@ -515,12 +516,4 @@ export class UserKeyRotationService {
|
|||||||
HashPurpose.ServerAuthorization,
|
HashPurpose.ServerAuthorization,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async firstValueFromOrThrow<T>(value: Observable<T>, name: string): Promise<T> {
|
|
||||||
const result = await firstValueFrom(value);
|
|
||||||
if (result == null) {
|
|
||||||
throw new Error(`Failed to get ${name}`);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
|
||||||
|
import {
|
||||||
|
MasterPasswordAuthenticationData,
|
||||||
|
MasterPasswordUnlockData,
|
||||||
|
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
|
|
||||||
export class OrganizationUserResetPasswordRequest {
|
export class OrganizationUserResetPasswordRequest {
|
||||||
newMasterPasswordHash: string;
|
newMasterPasswordHash: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
|
// This will eventually be changed to be an actual constructor, once all callers are updated.
|
||||||
|
// The body of this request will be changed to carry the authentication data and unlock data.
|
||||||
|
// https://bitwarden.atlassian.net/browse/PM-23234
|
||||||
|
static newConstructor(
|
||||||
|
authenticationData: MasterPasswordAuthenticationData,
|
||||||
|
unlockData: MasterPasswordUnlockData,
|
||||||
|
): OrganizationUserResetPasswordRequest {
|
||||||
|
const request = new OrganizationUserResetPasswordRequest();
|
||||||
|
request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash;
|
||||||
|
request.key = unlockData.masterKeyWrappedUserKey;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,8 +137,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
|||||||
newPasswordHint,
|
newPasswordHint,
|
||||||
orgSsoIdentifier,
|
orgSsoIdentifier,
|
||||||
keysRequest,
|
keysRequest,
|
||||||
kdfConfig.kdfType,
|
kdfConfig,
|
||||||
kdfConfig.iterations,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.masterPasswordApiService.setPassword(request);
|
await this.masterPasswordApiService.setPassword(request);
|
||||||
|
|||||||
@@ -157,8 +157,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
credentials.newPasswordHint,
|
credentials.newPasswordHint,
|
||||||
credentials.orgSsoIdentifier,
|
credentials.orgSsoIdentifier,
|
||||||
keysRequest,
|
keysRequest,
|
||||||
credentials.kdfConfig.kdfType,
|
credentials.kdfConfig,
|
||||||
credentials.kdfConfig.iterations,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ import { EncryptServiceImplementation } from "@bitwarden/common/key-management/c
|
|||||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||||
|
import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service";
|
||||||
|
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
|
||||||
|
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service";
|
||||||
|
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||||
import {
|
import {
|
||||||
@@ -1242,6 +1246,16 @@ const safeProviders: SafeProvider[] = [
|
|||||||
ConfigService,
|
ConfigService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ChangeKdfApiService,
|
||||||
|
useClass: DefaultChangeKdfApiService,
|
||||||
|
deps: [ApiServiceAbstraction],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ChangeKdfService,
|
||||||
|
useClass: DefaultChangeKdfService,
|
||||||
|
deps: [MasterPasswordServiceAbstraction, KeyService, KdfConfigService, ChangeKdfApiService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AuthRequestServiceAbstraction,
|
provide: AuthRequestServiceAbstraction,
|
||||||
useClass: AuthRequestService,
|
useClass: AuthRequestService,
|
||||||
|
|||||||
@@ -303,6 +303,14 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
throw new Error("KdfConfig is required to create master key.");
|
throw new Error("KdfConfig is required to create master key.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const salt =
|
||||||
|
this.userId != null
|
||||||
|
? await firstValueFrom(this.masterPasswordService.saltForUser$(this.userId))
|
||||||
|
: this.masterPasswordService.emailToSalt(this.email);
|
||||||
|
if (salt == null) {
|
||||||
|
throw new Error("Salt is required to create master key.");
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Verify current password is correct (if necessary)
|
// 2. Verify current password is correct (if necessary)
|
||||||
if (
|
if (
|
||||||
this.flow === InputPasswordFlow.ChangePassword ||
|
this.flow === InputPasswordFlow.ChangePassword ||
|
||||||
@@ -348,6 +356,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
|
|
||||||
const passwordInputResult: PasswordInputResult = {
|
const passwordInputResult: PasswordInputResult = {
|
||||||
newPassword,
|
newPassword,
|
||||||
|
salt,
|
||||||
newMasterKey,
|
newMasterKey,
|
||||||
newServerMasterKeyHash,
|
newServerMasterKeyHash,
|
||||||
newLocalMasterKeyHash,
|
newLocalMasterKeyHash,
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
|
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
import { KdfConfig } from "@bitwarden/key-management";
|
import { KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
export interface PasswordInputResult {
|
export interface PasswordInputResult {
|
||||||
currentPassword?: string;
|
currentPassword?: string;
|
||||||
|
newPassword: string;
|
||||||
|
kdfConfig?: KdfConfig;
|
||||||
|
salt?: MasterPasswordSalt;
|
||||||
|
newPasswordHint?: string;
|
||||||
|
rotateUserKey?: boolean;
|
||||||
|
|
||||||
|
/** @deprecated This low-level cryptographic state will be removed. It will be replaced by high level calls to masterpassword service, in the consumers of this interface. */
|
||||||
currentMasterKey?: MasterKey;
|
currentMasterKey?: MasterKey;
|
||||||
|
/** @deprecated */
|
||||||
currentServerMasterKeyHash?: string;
|
currentServerMasterKeyHash?: string;
|
||||||
|
/** @deprecated */
|
||||||
currentLocalMasterKeyHash?: string;
|
currentLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
newPassword: string;
|
/** @deprecated */
|
||||||
newPasswordHint?: string;
|
|
||||||
newMasterKey?: MasterKey;
|
newMasterKey?: MasterKey;
|
||||||
|
/** @deprecated */
|
||||||
newServerMasterKeyHash?: string;
|
newServerMasterKeyHash?: string;
|
||||||
|
/** @deprecated */
|
||||||
newLocalMasterKeyHash?: string;
|
newLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
kdfConfig?: KdfConfig;
|
|
||||||
rotateUserKey?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { FakeAccountService, makeEncString, mockAccountServiceWith } from "@bitwarden/common/spec";
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import {
|
import {
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
@@ -61,7 +61,7 @@ const masterPassword = "password";
|
|||||||
const deviceId = Utils.newGuid();
|
const deviceId = Utils.newGuid();
|
||||||
const accessToken = "ACCESS_TOKEN";
|
const accessToken = "ACCESS_TOKEN";
|
||||||
const refreshToken = "REFRESH_TOKEN";
|
const refreshToken = "REFRESH_TOKEN";
|
||||||
const encryptedUserKey = makeEncString("USER_KEY");
|
const encryptedUserKey = "USER_KEY";
|
||||||
const privateKey = "PRIVATE_KEY";
|
const privateKey = "PRIVATE_KEY";
|
||||||
const kdf = 0;
|
const kdf = 0;
|
||||||
const kdfIterations = 10000;
|
const kdfIterations = 10000;
|
||||||
@@ -76,7 +76,7 @@ const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerRe
|
|||||||
KdfType: kdf,
|
KdfType: kdf,
|
||||||
Iterations: kdfIterations,
|
Iterations: kdfIterations,
|
||||||
},
|
},
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export function identityTokenResponseFactory(
|
|||||||
ForcePasswordReset: false,
|
ForcePasswordReset: false,
|
||||||
Kdf: kdf,
|
Kdf: kdf,
|
||||||
KdfIterations: kdfIterations,
|
KdfIterations: kdfIterations,
|
||||||
Key: encryptedUserKey.encryptedString,
|
Key: encryptedUserKey,
|
||||||
PrivateKey: privateKey,
|
PrivateKey: privateKey,
|
||||||
ResetMasterPassword: false,
|
ResetMasterPassword: false,
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import {
|
||||||
|
MasterPasswordAuthenticationData,
|
||||||
|
MasterPasswordUnlockData,
|
||||||
|
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
|
|
||||||
import { EmailTokenRequest } from "./email-token.request";
|
import { EmailTokenRequest } from "./email-token.request";
|
||||||
|
|
||||||
export class EmailRequest extends EmailTokenRequest {
|
export class EmailRequest extends EmailTokenRequest {
|
||||||
newMasterPasswordHash: string;
|
newMasterPasswordHash: string;
|
||||||
token: string;
|
token: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
|
// This will eventually be changed to be an actual constructor, once all callers are updated.
|
||||||
|
// The body of this request will be changed to carry the authentication data and unlock data.
|
||||||
|
// https://bitwarden.atlassian.net/browse/PM-23234
|
||||||
|
static newConstructor(
|
||||||
|
authenticationData: MasterPasswordAuthenticationData,
|
||||||
|
unlockData: MasterPasswordUnlockData,
|
||||||
|
): EmailRequest {
|
||||||
|
const request = new EmailRequest();
|
||||||
|
request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash;
|
||||||
|
request.key = unlockData.masterKeyWrappedUserKey;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import {
|
||||||
|
MasterPasswordAuthenticationData,
|
||||||
|
MasterPasswordUnlockData,
|
||||||
|
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
|
|
||||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||||
|
|
||||||
export class PasswordRequest extends SecretVerificationRequest {
|
export class PasswordRequest extends SecretVerificationRequest {
|
||||||
newMasterPasswordHash: string;
|
newMasterPasswordHash: string;
|
||||||
masterPasswordHint: string;
|
masterPasswordHint: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
|
authenticationData?: MasterPasswordAuthenticationData;
|
||||||
|
unlockData?: MasterPasswordUnlockData;
|
||||||
|
|
||||||
|
// This will eventually be changed to be an actual constructor, once all callers are updated.
|
||||||
|
// https://bitwarden.atlassian.net/browse/PM-23234
|
||||||
|
static newConstructor(
|
||||||
|
authenticationData: MasterPasswordAuthenticationData,
|
||||||
|
unlockData: MasterPasswordUnlockData,
|
||||||
|
): PasswordRequest {
|
||||||
|
const request = new PasswordRequest();
|
||||||
|
request.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash;
|
||||||
|
request.key = unlockData.masterKeyWrappedUserKey;
|
||||||
|
request.authenticationData = authenticationData;
|
||||||
|
request.unlockData = unlockData;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
|
|
||||||
|
import { MasterPasswordAuthenticationData } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
|
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
export class SecretVerificationRequest {
|
export class SecretVerificationRequest {
|
||||||
masterPasswordHash: string;
|
masterPasswordHash: string;
|
||||||
otp: string;
|
otp: string;
|
||||||
authRequestAccessCode: string;
|
authRequestAccessCode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutates this request to include the master password authentication data, to authenticate the request.
|
||||||
|
*/
|
||||||
|
authenticateWith(
|
||||||
|
masterPasswordAuthenticationData: MasterPasswordAuthenticationData,
|
||||||
|
): SecretVerificationRequest {
|
||||||
|
this.masterPasswordHash = masterPasswordAuthenticationData.masterPasswordAuthenticationHash;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
|
|
||||||
|
import {
|
||||||
|
MasterPasswordAuthenticationData,
|
||||||
|
MasterPasswordUnlockData,
|
||||||
|
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KdfType } from "@bitwarden/key-management";
|
import { KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { KeysRequest } from "../../../models/request/keys.request";
|
import { KeysRequest } from "../../../models/request/keys.request";
|
||||||
|
|
||||||
@@ -21,19 +26,45 @@ export class SetPasswordRequest {
|
|||||||
masterPasswordHint: string,
|
masterPasswordHint: string,
|
||||||
orgIdentifier: string,
|
orgIdentifier: string,
|
||||||
keys: KeysRequest | null,
|
keys: KeysRequest | null,
|
||||||
kdf: KdfType,
|
kdf: KdfConfig,
|
||||||
kdfIterations: number,
|
|
||||||
kdfMemory?: number,
|
|
||||||
kdfParallelism?: number,
|
|
||||||
) {
|
) {
|
||||||
this.masterPasswordHash = masterPasswordHash;
|
this.masterPasswordHash = masterPasswordHash;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.masterPasswordHint = masterPasswordHint;
|
this.masterPasswordHint = masterPasswordHint;
|
||||||
this.kdf = kdf;
|
|
||||||
this.kdfIterations = kdfIterations;
|
|
||||||
this.kdfMemory = kdfMemory;
|
|
||||||
this.kdfParallelism = kdfParallelism;
|
|
||||||
this.orgIdentifier = orgIdentifier;
|
this.orgIdentifier = orgIdentifier;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
|
|
||||||
|
if (kdf.kdfType === KdfType.PBKDF2_SHA256) {
|
||||||
|
this.kdf = KdfType.PBKDF2_SHA256;
|
||||||
|
this.kdfIterations = kdf.iterations;
|
||||||
|
} else if (kdf.kdfType === KdfType.Argon2id) {
|
||||||
|
this.kdf = KdfType.Argon2id;
|
||||||
|
this.kdfIterations = kdf.iterations;
|
||||||
|
this.kdfMemory = kdf.memory;
|
||||||
|
this.kdfParallelism = kdf.parallelism;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported KDF type: ${kdf}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will eventually be changed to be an actual constructor, once all callers are updated.
|
||||||
|
// The body of this request will be changed to carry the authentication data and unlock data.
|
||||||
|
// https://bitwarden.atlassian.net/browse/PM-23234
|
||||||
|
static newConstructor(
|
||||||
|
authenticationData: MasterPasswordAuthenticationData,
|
||||||
|
unlockData: MasterPasswordUnlockData,
|
||||||
|
masterPasswordHint: string,
|
||||||
|
orgIdentifier: string,
|
||||||
|
keys: KeysRequest | null,
|
||||||
|
): SetPasswordRequest {
|
||||||
|
const request = new SetPasswordRequest(
|
||||||
|
authenticationData.masterPasswordAuthenticationHash,
|
||||||
|
unlockData.masterKeyWrappedUserKey,
|
||||||
|
masterPasswordHint,
|
||||||
|
orgIdentifier,
|
||||||
|
keys,
|
||||||
|
unlockData.kdf,
|
||||||
|
);
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KdfType } from "@bitwarden/key-management";
|
import { KdfType } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { makeEncString } from "../../../../../spec";
|
|
||||||
|
|
||||||
import { UserDecryptionOptionsResponse } from "./user-decryption-options.response";
|
import { UserDecryptionOptionsResponse } from "./user-decryption-options.response";
|
||||||
|
|
||||||
describe("UserDecryptionOptionsResponse", () => {
|
describe("UserDecryptionOptionsResponse", () => {
|
||||||
it("should create response when master password unlock is present", () => {
|
it("should create response when master password unlock is present", () => {
|
||||||
const salt = "test@example.com";
|
const salt = "test@example.com";
|
||||||
const encryptedUserKey = makeEncString("testUserKey");
|
const encryptedUserKey = "testUserKey";
|
||||||
|
|
||||||
const response = new UserDecryptionOptionsResponse({
|
const response = new UserDecryptionOptionsResponse({
|
||||||
HasMasterPassword: true,
|
HasMasterPassword: true,
|
||||||
@@ -18,7 +16,7 @@ describe("UserDecryptionOptionsResponse", () => {
|
|||||||
KdfType: KdfType.PBKDF2_SHA256,
|
KdfType: KdfType.PBKDF2_SHA256,
|
||||||
Iterations: 600_000,
|
Iterations: 600_000,
|
||||||
},
|
},
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KdfType } from "@bitwarden/key-management";
|
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { PasswordRequest } from "../../models/request/password.request";
|
import { PasswordRequest } from "../../models/request/password.request";
|
||||||
import { SetPasswordRequest } from "../../models/request/set-password.request";
|
import { SetPasswordRequest } from "../../models/request/set-password.request";
|
||||||
@@ -42,8 +42,7 @@ describe("MasterPasswordApiService", () => {
|
|||||||
publicKey: "publicKey",
|
publicKey: "publicKey",
|
||||||
encryptedPrivateKey: "encryptedPrivateKey",
|
encryptedPrivateKey: "encryptedPrivateKey",
|
||||||
},
|
},
|
||||||
KdfType.PBKDF2_SHA256,
|
new PBKDF2KdfConfig(600_000),
|
||||||
600_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||||
|
|
||||||
|
export abstract class ChangeKdfApiService {
|
||||||
|
/**
|
||||||
|
* Sends a request to update the user's KDF parameters.
|
||||||
|
* @param request The KDF request containing authentication data, unlock data, and old authentication data
|
||||||
|
*/
|
||||||
|
abstract updateUserKdfParams(request: KdfRequest): Promise<void>;
|
||||||
|
}
|
||||||
15
libs/common/src/key-management/kdf/change-kdf-api.service.ts
Normal file
15
libs/common/src/key-management/kdf/change-kdf-api.service.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||||
|
|
||||||
|
import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class DefaultChangeKdfApiService implements ChangeKdfApiService {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
async updateUserKdfParams(request: KdfRequest): Promise<void> {
|
||||||
|
return this.apiService.send("POST", "/accounts/kdf", request, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
export abstract class ChangeKdfService {
|
||||||
|
/**
|
||||||
|
* Updates the user's KDF parameters
|
||||||
|
* @param masterPassword The user's current master password
|
||||||
|
* @param kdf The new KDF configuration to apply
|
||||||
|
* @param userId The ID of the user whose KDF parameters are being updated
|
||||||
|
* @throws If any of the parameters is null
|
||||||
|
* @throws If the user is locked or logged out
|
||||||
|
* @throws If the kdf change request fails
|
||||||
|
*/
|
||||||
|
abstract updateUserKdfParams(
|
||||||
|
masterPassword: string,
|
||||||
|
kdf: KdfConfig,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
167
libs/common/src/key-management/kdf/change-kdf-service.spec.ts
Normal file
167
libs/common/src/key-management/kdf/change-kdf-service.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction";
|
||||||
|
import {
|
||||||
|
MasterKeyWrappedUserKey,
|
||||||
|
MasterPasswordAuthenticationHash,
|
||||||
|
MasterPasswordSalt,
|
||||||
|
MasterPasswordUnlockData,
|
||||||
|
} from "../master-password/types/master-password.types";
|
||||||
|
|
||||||
|
import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction";
|
||||||
|
import { DefaultChangeKdfService } from "./change-kdf-service";
|
||||||
|
|
||||||
|
describe("ChangeKdfService", () => {
|
||||||
|
const changeKdfApiService = mock<ChangeKdfApiService>();
|
||||||
|
const masterPasswordService = mock<MasterPasswordServiceAbstraction>();
|
||||||
|
const keyService = mock<KeyService>();
|
||||||
|
const kdfConfigService = mock<KdfConfigService>();
|
||||||
|
|
||||||
|
let sut: DefaultChangeKdfService = mock<DefaultChangeKdfService>();
|
||||||
|
|
||||||
|
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||||
|
const mockOldKdfConfig = new PBKDF2KdfConfig(100000);
|
||||||
|
const mockNewKdfConfig = new PBKDF2KdfConfig(200000);
|
||||||
|
const mockOldHash = "oldHash" as MasterPasswordAuthenticationHash;
|
||||||
|
const mockNewHash = "newHash" as MasterPasswordAuthenticationHash;
|
||||||
|
const mockUserId = "00000000-0000-0000-0000-000000000000" as UserId;
|
||||||
|
const mockSalt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||||
|
const mockWrappedUserKey = "wrappedUserKey";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sut = new DefaultChangeKdfService(
|
||||||
|
masterPasswordService,
|
||||||
|
keyService,
|
||||||
|
kdfConfigService,
|
||||||
|
changeKdfApiService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateUserKdfParams", () => {
|
||||||
|
it("should throw an error if masterPassword is null", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams(null as unknown as string, mockNewKdfConfig, mockUserId),
|
||||||
|
).rejects.toThrow("masterPassword");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if masterPassword is undefined", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams(undefined as unknown as string, mockNewKdfConfig, mockUserId),
|
||||||
|
).rejects.toThrow("masterPassword");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if kdf is null", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams("masterPassword", null as unknown as PBKDF2KdfConfig, mockUserId),
|
||||||
|
).rejects.toThrow("kdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if kdf is undefined", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams(
|
||||||
|
"masterPassword",
|
||||||
|
undefined as unknown as PBKDF2KdfConfig,
|
||||||
|
mockUserId,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("kdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if userId is null", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, null as unknown as UserId),
|
||||||
|
).rejects.toThrow("userId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if userId is undefined", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, undefined as unknown as UserId),
|
||||||
|
).rejects.toThrow("userId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if userKey is null", async () => {
|
||||||
|
keyService.userKey$.mockReturnValueOnce(of(null));
|
||||||
|
masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt));
|
||||||
|
kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig));
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if salt is null", async () => {
|
||||||
|
keyService.userKey$.mockReturnValueOnce(of(mockUserKey));
|
||||||
|
masterPasswordService.saltForUser$.mockReturnValueOnce(of(null));
|
||||||
|
kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig));
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId),
|
||||||
|
).rejects.toThrow("Failed to get salt");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if oldKdfConfig is null", async () => {
|
||||||
|
keyService.userKey$.mockReturnValueOnce(of(mockUserKey));
|
||||||
|
masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt));
|
||||||
|
kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(null));
|
||||||
|
await expect(
|
||||||
|
sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId),
|
||||||
|
).rejects.toThrow("Failed to get oldKdfConfig");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call apiService.send with correct parameters", async () => {
|
||||||
|
keyService.userKey$.mockReturnValueOnce(of(mockUserKey));
|
||||||
|
masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt));
|
||||||
|
kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig));
|
||||||
|
|
||||||
|
masterPasswordService.makeMasterPasswordAuthenticationData
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
salt: mockSalt,
|
||||||
|
kdf: mockOldKdfConfig,
|
||||||
|
masterPasswordAuthenticationHash: mockOldHash,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
salt: mockSalt,
|
||||||
|
kdf: mockNewKdfConfig,
|
||||||
|
masterPasswordAuthenticationHash: mockNewHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValueOnce(
|
||||||
|
new MasterPasswordUnlockData(
|
||||||
|
mockSalt,
|
||||||
|
mockNewKdfConfig,
|
||||||
|
mockWrappedUserKey as MasterKeyWrappedUserKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId);
|
||||||
|
|
||||||
|
const expected = new KdfRequest(
|
||||||
|
{
|
||||||
|
salt: mockSalt,
|
||||||
|
kdf: mockNewKdfConfig,
|
||||||
|
masterPasswordAuthenticationHash: mockNewHash,
|
||||||
|
},
|
||||||
|
new MasterPasswordUnlockData(
|
||||||
|
mockSalt,
|
||||||
|
mockNewKdfConfig,
|
||||||
|
mockWrappedUserKey as MasterKeyWrappedUserKey,
|
||||||
|
),
|
||||||
|
).authenticateWith({
|
||||||
|
salt: mockSalt,
|
||||||
|
kdf: mockOldKdfConfig,
|
||||||
|
masterPasswordAuthenticationHash: mockOldHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(changeKdfApiService.updateUserKdfParams).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
59
libs/common/src/key-management/kdf/change-kdf-service.ts
Normal file
59
libs/common/src/key-management/kdf/change-kdf-service.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||||
|
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction";
|
||||||
|
import { firstValueFromOrThrow } from "../utils";
|
||||||
|
|
||||||
|
import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction";
|
||||||
|
import { ChangeKdfService } from "./change-kdf-service.abstraction";
|
||||||
|
|
||||||
|
export class DefaultChangeKdfService implements ChangeKdfService {
|
||||||
|
constructor(
|
||||||
|
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||||
|
private keyService: KeyService,
|
||||||
|
private kdfConfigService: KdfConfigService,
|
||||||
|
private changeKdfApiService: ChangeKdfApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async updateUserKdfParams(masterPassword: string, kdf: KdfConfig, userId: UserId): Promise<void> {
|
||||||
|
assertNonNullish(masterPassword, "masterPassword");
|
||||||
|
assertNonNullish(kdf, "kdf");
|
||||||
|
assertNonNullish(userId, "userId");
|
||||||
|
|
||||||
|
const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey");
|
||||||
|
const salt = await firstValueFromOrThrow(
|
||||||
|
this.masterPasswordService.saltForUser$(userId),
|
||||||
|
"salt",
|
||||||
|
);
|
||||||
|
const oldKdfConfig = await firstValueFromOrThrow(
|
||||||
|
this.kdfConfigService.getKdfConfig$(userId),
|
||||||
|
"oldKdfConfig",
|
||||||
|
);
|
||||||
|
|
||||||
|
const oldAuthenticationData =
|
||||||
|
await this.masterPasswordService.makeMasterPasswordAuthenticationData(
|
||||||
|
masterPassword,
|
||||||
|
oldKdfConfig,
|
||||||
|
salt,
|
||||||
|
);
|
||||||
|
const authenticationData =
|
||||||
|
await this.masterPasswordService.makeMasterPasswordAuthenticationData(
|
||||||
|
masterPassword,
|
||||||
|
kdf,
|
||||||
|
salt,
|
||||||
|
);
|
||||||
|
const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
|
||||||
|
masterPassword,
|
||||||
|
kdf,
|
||||||
|
salt,
|
||||||
|
userKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = new KdfRequest(authenticationData, unlockData);
|
||||||
|
request.authenticateWith(oldAuthenticationData);
|
||||||
|
await this.changeKdfApiService.updateUserKdfParams(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,11 @@ export abstract class MasterPasswordServiceAbstraction {
|
|||||||
* @throws If the user ID is provided, but the user is not found.
|
* @throws If the user ID is provided, but the user is not found.
|
||||||
*/
|
*/
|
||||||
abstract saltForUser$: (userId: UserId) => Observable<MasterPasswordSalt>;
|
abstract saltForUser$: (userId: UserId) => Observable<MasterPasswordSalt>;
|
||||||
|
/**
|
||||||
|
* Converts an email to a master password salt. This is a canonical encoding of the
|
||||||
|
* email, no matter how the email is capitalized.
|
||||||
|
*/
|
||||||
|
abstract emailToSalt(email: string): MasterPasswordSalt;
|
||||||
/**
|
/**
|
||||||
* An observable that emits the master key for the user.
|
* An observable that emits the master key for the user.
|
||||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
import { KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { makeEncString } from "../../../../../spec";
|
|
||||||
|
|
||||||
import { MasterPasswordUnlockResponse } from "./master-password-unlock.response";
|
import { MasterPasswordUnlockResponse } from "./master-password-unlock.response";
|
||||||
|
|
||||||
describe("MasterPasswordUnlockResponse", () => {
|
describe("MasterPasswordUnlockResponse", () => {
|
||||||
const salt = "test@example.com";
|
const salt = "test@example.com";
|
||||||
const encryptedUserKey = makeEncString("testUserKey");
|
const encryptedUserKey = "testUserKey";
|
||||||
const testKdfResponse = { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000 };
|
const testKdfResponse = { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000 };
|
||||||
|
|
||||||
it("should throw error when salt is not provided", () => {
|
it("should throw error when salt is not provided", () => {
|
||||||
@@ -15,7 +13,7 @@ describe("MasterPasswordUnlockResponse", () => {
|
|||||||
new MasterPasswordUnlockResponse({
|
new MasterPasswordUnlockResponse({
|
||||||
Salt: undefined,
|
Salt: undefined,
|
||||||
Kdf: testKdfResponse,
|
Kdf: testKdfResponse,
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
});
|
});
|
||||||
}).toThrow("MasterPasswordUnlockResponse does not contain a valid salt");
|
}).toThrow("MasterPasswordUnlockResponse does not contain a valid salt");
|
||||||
});
|
});
|
||||||
@@ -36,7 +34,7 @@ describe("MasterPasswordUnlockResponse", () => {
|
|||||||
const response = new MasterPasswordUnlockResponse({
|
const response = new MasterPasswordUnlockResponse({
|
||||||
Salt: salt,
|
Salt: salt,
|
||||||
Kdf: testKdfResponse,
|
Kdf: testKdfResponse,
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.salt).toBe(salt);
|
expect(response.salt).toBe(salt);
|
||||||
@@ -50,7 +48,7 @@ describe("MasterPasswordUnlockResponse", () => {
|
|||||||
const response = new MasterPasswordUnlockResponse({
|
const response = new MasterPasswordUnlockResponse({
|
||||||
Salt: salt,
|
Salt: salt,
|
||||||
Kdf: testKdfResponse,
|
Kdf: testKdfResponse,
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlockData = response.toMasterPasswordUnlockData();
|
const unlockData = response.toMasterPasswordUnlockData();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BaseResponse } from "../../../../models/response/base.response";
|
import { BaseResponse } from "../../../../models/response/base.response";
|
||||||
import { EncString } from "../../../crypto/models/enc-string";
|
|
||||||
import { KdfConfigResponse } from "../../../models/response/kdf-config.response";
|
import { KdfConfigResponse } from "../../../models/response/kdf-config.response";
|
||||||
import {
|
import {
|
||||||
MasterKeyWrappedUserKey,
|
MasterKeyWrappedUserKey,
|
||||||
@@ -29,9 +28,7 @@ export class MasterPasswordUnlockResponse extends BaseResponse {
|
|||||||
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
|
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.masterKeyWrappedUserKey = new EncString(
|
this.masterKeyWrappedUserKey = masterKeyEncryptedUserKey as MasterKeyWrappedUserKey;
|
||||||
masterKeyEncryptedUserKey,
|
|
||||||
) as MasterKeyWrappedUserKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toMasterPasswordUnlockData() {
|
toMasterPasswordUnlockData() {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
|||||||
this.masterKeyHashSubject.next(initialMasterKeyHash);
|
this.masterKeyHashSubject.next(initialMasterKeyHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailToSalt(email: string): MasterPasswordSalt {
|
||||||
|
return this.mock.emailToSalt(email);
|
||||||
|
}
|
||||||
|
|
||||||
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
||||||
return this.mock.saltForUser$(userId);
|
return this.mock.saltForUser$(userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FakeAccountService,
|
FakeAccountService,
|
||||||
makeEncString,
|
|
||||||
makeSymmetricCryptoKey,
|
makeSymmetricCryptoKey,
|
||||||
mockAccountServiceWith,
|
mockAccountServiceWith,
|
||||||
} from "../../../../spec";
|
} from "../../../../spec";
|
||||||
@@ -385,7 +384,7 @@ describe("MasterPasswordService", () => {
|
|||||||
const kdfPBKDF2: KdfConfig = new PBKDF2KdfConfig(600_000);
|
const kdfPBKDF2: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||||
const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3);
|
const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3);
|
||||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||||
const encryptedUserKey = makeEncString("testUserKet") as MasterKeyWrappedUserKey;
|
const encryptedUserKey = "testUserKet" as MasterKeyWrappedUserKey;
|
||||||
|
|
||||||
it("returns null when value is null", () => {
|
it("returns null when value is null", () => {
|
||||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(
|
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(
|
||||||
@@ -401,7 +400,7 @@ describe("MasterPasswordService", () => {
|
|||||||
kdfType: KdfType.PBKDF2_SHA256,
|
kdfType: KdfType.PBKDF2_SHA256,
|
||||||
iterations: kdfPBKDF2.iterations,
|
iterations: kdfPBKDF2.iterations,
|
||||||
},
|
},
|
||||||
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
|
masterKeyWrappedUserKey: encryptedUserKey as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
||||||
@@ -419,7 +418,7 @@ describe("MasterPasswordService", () => {
|
|||||||
memory: kdfArgon2.memory,
|
memory: kdfArgon2.memory,
|
||||||
parallelism: kdfArgon2.parallelism,
|
parallelism: kdfArgon2.parallelism,
|
||||||
},
|
},
|
||||||
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
|
masterKeyWrappedUserKey: encryptedUserKey as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
|||||||
return EncString.fromJSON(key);
|
return EncString.fromJSON(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emailToSalt(email: string): MasterPasswordSalt {
|
emailToSalt(email: string): MasterPasswordSalt {
|
||||||
return email.toLowerCase().trim() as MasterPasswordSalt;
|
return email.toLowerCase().trim() as MasterPasswordSalt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +256,9 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
|||||||
assertNonNullish(password, "password");
|
assertNonNullish(password, "password");
|
||||||
assertNonNullish(kdf, "kdf");
|
assertNonNullish(kdf, "kdf");
|
||||||
assertNonNullish(salt, "salt");
|
assertNonNullish(salt, "salt");
|
||||||
|
if (password === "") {
|
||||||
|
throw new Error("Master password cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||||
@@ -294,18 +297,19 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
|||||||
assertNonNullish(kdf, "kdf");
|
assertNonNullish(kdf, "kdf");
|
||||||
assertNonNullish(salt, "salt");
|
assertNonNullish(salt, "salt");
|
||||||
assertNonNullish(userKey, "userKey");
|
assertNonNullish(userKey, "userKey");
|
||||||
|
if (password === "") {
|
||||||
|
throw new Error("Master password cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||||
|
|
||||||
await SdkLoadService.Ready;
|
await SdkLoadService.Ready;
|
||||||
const masterKeyWrappedUserKey = new EncString(
|
const masterKeyWrappedUserKey = PureCrypto.encrypt_user_key_with_master_password(
|
||||||
PureCrypto.encrypt_user_key_with_master_password(
|
userKey.toEncoded(),
|
||||||
userKey.toEncoded(),
|
password,
|
||||||
password,
|
salt,
|
||||||
salt,
|
kdf.toSdkConfig(),
|
||||||
kdf.toSdkConfig(),
|
|
||||||
),
|
|
||||||
) as MasterKeyWrappedUserKey;
|
) as MasterKeyWrappedUserKey;
|
||||||
return new MasterPasswordUnlockData(salt, kdf, masterKeyWrappedUserKey);
|
return new MasterPasswordUnlockData(salt, kdf, masterKeyWrappedUserKey);
|
||||||
}
|
}
|
||||||
@@ -320,7 +324,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
|||||||
await SdkLoadService.Ready;
|
await SdkLoadService.Ready;
|
||||||
const userKey = new SymmetricCryptoKey(
|
const userKey = new SymmetricCryptoKey(
|
||||||
PureCrypto.decrypt_user_key_with_master_password(
|
PureCrypto.decrypt_user_key_with_master_password(
|
||||||
masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString,
|
masterPasswordUnlockData.masterKeyWrappedUserKey,
|
||||||
password,
|
password,
|
||||||
masterPasswordUnlockData.salt,
|
masterPasswordUnlockData.salt,
|
||||||
masterPasswordUnlockData.kdf.toSdkConfig(),
|
masterPasswordUnlockData.kdf.toSdkConfig(),
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Jsonify, Opaque } from "type-fest";
|
|||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||||
|
import { EncString } from "@bitwarden/sdk-internal";
|
||||||
import { EncString } from "../../crypto/models/enc-string";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Base64-encoded master password authentication hash, that is sent to the server for authentication.
|
* The Base64-encoded master password authentication hash, that is sent to the server for authentication.
|
||||||
@@ -13,7 +12,7 @@ export type MasterPasswordAuthenticationHash = Opaque<string, "MasterPasswordAut
|
|||||||
* You MUST obtain this through the emailToSalt function in MasterPasswordService
|
* You MUST obtain this through the emailToSalt function in MasterPasswordService
|
||||||
*/
|
*/
|
||||||
export type MasterPasswordSalt = Opaque<string, "MasterPasswordSalt">;
|
export type MasterPasswordSalt = Opaque<string, "MasterPasswordSalt">;
|
||||||
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterPasswordSalt">;
|
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterKeyWrappedUserKey">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data required to unlock with the master password.
|
* The data required to unlock with the master password.
|
||||||
@@ -29,7 +28,7 @@ export class MasterPasswordUnlockData {
|
|||||||
return {
|
return {
|
||||||
salt: this.salt,
|
salt: this.salt,
|
||||||
kdf: this.kdf,
|
kdf: this.kdf,
|
||||||
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey.toJSON(),
|
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ export class MasterPasswordUnlockData {
|
|||||||
obj.kdf.kdfType === KdfType.PBKDF2_SHA256
|
obj.kdf.kdfType === KdfType.PBKDF2_SHA256
|
||||||
? PBKDF2KdfConfig.fromJSON(obj.kdf)
|
? PBKDF2KdfConfig.fromJSON(obj.kdf)
|
||||||
: Argon2KdfConfig.fromJSON(obj.kdf),
|
: Argon2KdfConfig.fromJSON(obj.kdf),
|
||||||
EncString.fromJSON(obj.masterKeyWrappedUserKey) as MasterKeyWrappedUserKey,
|
obj.masterKeyWrappedUserKey as MasterKeyWrappedUserKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KdfType } from "@bitwarden/key-management";
|
import { KdfType } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { makeEncString } from "../../../../spec";
|
|
||||||
|
|
||||||
import { UserDecryptionResponse } from "./user-decryption.response";
|
import { UserDecryptionResponse } from "./user-decryption.response";
|
||||||
|
|
||||||
describe("UserDecryptionResponse", () => {
|
describe("UserDecryptionResponse", () => {
|
||||||
it("should create response when masterPasswordUnlock provided", () => {
|
it("should create response when masterPasswordUnlock provided", () => {
|
||||||
const salt = "test@example.com";
|
const salt = "test@example.com";
|
||||||
const encryptedUserKey = makeEncString("testUserKey");
|
const encryptedUserKey = "testUserKey";
|
||||||
const kdfIterations = 600_000;
|
const kdfIterations = 600_000;
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
@@ -18,7 +16,7 @@ describe("UserDecryptionResponse", () => {
|
|||||||
KdfType: KdfType.PBKDF2_SHA256 as number,
|
KdfType: KdfType.PBKDF2_SHA256 as number,
|
||||||
Iterations: kdfIterations,
|
Iterations: kdfIterations,
|
||||||
},
|
},
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
12
libs/common/src/key-management/utils.ts
Normal file
12
libs/common/src/key-management/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
|
export async function firstValueFromOrThrow<T>(
|
||||||
|
value: Observable<T | null>,
|
||||||
|
name: string,
|
||||||
|
): Promise<T> {
|
||||||
|
const result = await firstValueFrom(value);
|
||||||
|
if (result == null) {
|
||||||
|
throw new Error(`Failed to get ${name}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import {
|
||||||
// @ts-strict-ignore
|
MasterPasswordAuthenticationData,
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
MasterPasswordUnlockData,
|
||||||
// eslint-disable-next-line no-restricted-imports
|
} from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||||
import { KdfType } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { PasswordRequest } from "../../auth/models/request/password.request";
|
import { PasswordRequest } from "../../auth/models/request/password.request";
|
||||||
|
|
||||||
export class KdfRequest extends PasswordRequest {
|
export class KdfRequest extends PasswordRequest {
|
||||||
kdf: KdfType;
|
constructor(
|
||||||
kdfIterations: number;
|
authenticationData: MasterPasswordAuthenticationData,
|
||||||
kdfMemory?: number;
|
unlockData: MasterPasswordUnlockData,
|
||||||
kdfParallelism?: number;
|
) {
|
||||||
|
super();
|
||||||
|
// Note, this init code should be in the super constructor, once PasswordRequest's constructor is updated.
|
||||||
|
this.newMasterPasswordHash = authenticationData.masterPasswordAuthenticationHash;
|
||||||
|
this.key = unlockData.masterKeyWrappedUserKey;
|
||||||
|
this.authenticationData = authenticationData;
|
||||||
|
this.unlockData = unlockData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { makeEncString } from "../../../spec";
|
|
||||||
import { Matrix } from "../../../spec/matrix";
|
import { Matrix } from "../../../spec/matrix";
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -247,7 +246,7 @@ describe("DefaultSyncService", () => {
|
|||||||
describe("syncUserDecryption", () => {
|
describe("syncUserDecryption", () => {
|
||||||
const salt = "test@example.com";
|
const salt = "test@example.com";
|
||||||
const kdf = new PBKDF2KdfConfig(600_000);
|
const kdf = new PBKDF2KdfConfig(600_000);
|
||||||
const encryptedUserKey = makeEncString("testUserKey");
|
const encryptedUserKey = "testUserKey";
|
||||||
|
|
||||||
it("should set master password unlock when present in user decryption", async () => {
|
it("should set master password unlock when present in user decryption", async () => {
|
||||||
const syncResponse = new SyncResponse({
|
const syncResponse = new SyncResponse({
|
||||||
@@ -261,7 +260,7 @@ describe("DefaultSyncService", () => {
|
|||||||
KdfType: kdf.kdfType,
|
KdfType: kdf.kdfType,
|
||||||
Iterations: kdf.iterations,
|
Iterations: kdf.iterations,
|
||||||
},
|
},
|
||||||
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
|
MasterKeyEncryptedUserKey: encryptedUserKey,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user