1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +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:
Bernd Schoolmann
2025-09-24 05:10:54 +09:00
committed by GitHub
parent 6001980dc5
commit 4b73198ce5
33 changed files with 507 additions and 117 deletions

View File

@@ -131,8 +131,7 @@ describe("WebSetInitialPasswordService", () => {
credentials.newPasswordHint,
credentials.orgSsoIdentifier,
keysRequest,
credentials.kdfConfig.kdfType,
credentials.kdfConfig.iterations,
credentials.kdfConfig,
);
});

View File

@@ -4,13 +4,13 @@ import { Component, Inject } from "@angular/core";
import { FormGroup, FormControl, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management";
import { KdfConfig, KdfType } from "@bitwarden/key-management";
@Component({
selector: "app-change-kdf-confirmation",
@@ -28,13 +28,12 @@ export class ChangeKdfConfirmationComponent {
loading = false;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private keyService: KeyService,
private messagingService: MessagingService,
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
private accountService: AccountService,
private toastService: ToastService,
private changeKdfService: ChangeKdfService,
) {
this.kdfConfig = params.kdfConfig;
this.masterPassword = null;
@@ -56,37 +55,17 @@ export class ChangeKdfConfirmationComponent {
};
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;
// Ensure the KDF config is valid.
this.kdfConfig.validateKdfConfigForSetting();
const request = new KdfRequest();
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 activeAccountId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const newMasterKey = await this.keyService.makeMasterKey(
await this.changeKdfService.updateUserKdfParams(
masterPassword,
activeAccount.email,
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);
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { firstValueFrom } from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
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 { 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 { firstValueFromOrThrow } from "@bitwarden/common/key-management/utils";
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -103,18 +104,18 @@ export class UserKeyRotationService {
}
// Read current cryptographic state / settings
const masterKeyKdfConfig: KdfConfig = (await this.firstValueFromOrThrow(
const masterKeyKdfConfig: KdfConfig = (await firstValueFromOrThrow(
this.kdfConfigService.getKdfConfig$(user.id),
"KDF config",
))!;
// The masterkey salt used for deriving the masterkey always needs to be trimmed and lowercased.
const masterKeySalt = user.email.trim().toLowerCase();
const currentUserKey: UserKey = (await this.firstValueFromOrThrow(
const currentUserKey: UserKey = (await firstValueFromOrThrow(
this.keyService.userKey$(user.id),
"User key",
))!;
const currentUserKeyWrappedPrivateKey = new EncString(
(await this.firstValueFromOrThrow(
(await firstValueFromOrThrow(
this.keyService.userEncryptedPrivateKey$(user.id),
"User encrypted private key",
))!,
@@ -515,12 +516,4 @@ export class UserKeyRotationService {
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;
}
}