1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 19:23:52 +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

@@ -27,6 +27,11 @@ export abstract class MasterPasswordServiceAbstraction {
* @throws If the user ID is provided, but the user is not found.
*/
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.
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.

View File

@@ -1,13 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import { KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { makeEncString } from "../../../../../spec";
import { MasterPasswordUnlockResponse } from "./master-password-unlock.response";
describe("MasterPasswordUnlockResponse", () => {
const salt = "test@example.com";
const encryptedUserKey = makeEncString("testUserKey");
const encryptedUserKey = "testUserKey";
const testKdfResponse = { KdfType: KdfType.PBKDF2_SHA256, Iterations: 600_000 };
it("should throw error when salt is not provided", () => {
@@ -15,7 +13,7 @@ describe("MasterPasswordUnlockResponse", () => {
new MasterPasswordUnlockResponse({
Salt: undefined,
Kdf: testKdfResponse,
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
MasterKeyEncryptedUserKey: encryptedUserKey,
});
}).toThrow("MasterPasswordUnlockResponse does not contain a valid salt");
});
@@ -36,7 +34,7 @@ describe("MasterPasswordUnlockResponse", () => {
const response = new MasterPasswordUnlockResponse({
Salt: salt,
Kdf: testKdfResponse,
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
MasterKeyEncryptedUserKey: encryptedUserKey,
});
expect(response.salt).toBe(salt);
@@ -50,7 +48,7 @@ describe("MasterPasswordUnlockResponse", () => {
const response = new MasterPasswordUnlockResponse({
Salt: salt,
Kdf: testKdfResponse,
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
MasterKeyEncryptedUserKey: encryptedUserKey,
});
const unlockData = response.toMasterPasswordUnlockData();

View File

@@ -1,5 +1,4 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { EncString } from "../../../crypto/models/enc-string";
import { KdfConfigResponse } from "../../../models/response/kdf-config.response";
import {
MasterKeyWrappedUserKey,
@@ -29,9 +28,7 @@ export class MasterPasswordUnlockResponse extends BaseResponse {
"MasterPasswordUnlockResponse does not contain a valid master key encrypted user key",
);
}
this.masterKeyWrappedUserKey = new EncString(
masterKeyEncryptedUserKey,
) as MasterKeyWrappedUserKey;
this.masterKeyWrappedUserKey = masterKeyEncryptedUserKey as MasterKeyWrappedUserKey;
}
toMasterPasswordUnlockData() {

View File

@@ -33,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
this.masterKeyHashSubject.next(initialMasterKeyHash);
}
emailToSalt(email: string): MasterPasswordSalt {
return this.mock.emailToSalt(email);
}
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
return this.mock.saltForUser$(userId);
}

View File

@@ -10,7 +10,6 @@ import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden
import {
FakeAccountService,
makeEncString,
makeSymmetricCryptoKey,
mockAccountServiceWith,
} from "../../../../spec";
@@ -385,7 +384,7 @@ describe("MasterPasswordService", () => {
const kdfPBKDF2: KdfConfig = new PBKDF2KdfConfig(600_000);
const kdfArgon2: KdfConfig = new Argon2KdfConfig(4, 64, 3);
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", () => {
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(
@@ -401,7 +400,7 @@ describe("MasterPasswordService", () => {
kdfType: KdfType.PBKDF2_SHA256,
iterations: kdfPBKDF2.iterations,
},
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
masterKeyWrappedUserKey: encryptedUserKey as string,
};
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);
@@ -419,7 +418,7 @@ describe("MasterPasswordService", () => {
memory: kdfArgon2.memory,
parallelism: kdfArgon2.parallelism,
},
masterKeyWrappedUserKey: encryptedUserKey.encryptedString as string,
masterKeyWrappedUserKey: encryptedUserKey as string,
};
const deserialized = MASTER_PASSWORD_UNLOCK_KEY.deserializer(data);

View File

@@ -132,7 +132,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
return EncString.fromJSON(key);
}
private emailToSalt(email: string): MasterPasswordSalt {
emailToSalt(email: string): MasterPasswordSalt {
return email.toLowerCase().trim() as MasterPasswordSalt;
}
@@ -256,6 +256,9 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
assertNonNullish(password, "password");
assertNonNullish(kdf, "kdf");
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.
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
@@ -294,18 +297,19 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
assertNonNullish(kdf, "kdf");
assertNonNullish(salt, "salt");
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.
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
await SdkLoadService.Ready;
const masterKeyWrappedUserKey = new EncString(
PureCrypto.encrypt_user_key_with_master_password(
userKey.toEncoded(),
password,
salt,
kdf.toSdkConfig(),
),
const masterKeyWrappedUserKey = PureCrypto.encrypt_user_key_with_master_password(
userKey.toEncoded(),
password,
salt,
kdf.toSdkConfig(),
) as MasterKeyWrappedUserKey;
return new MasterPasswordUnlockData(salt, kdf, masterKeyWrappedUserKey);
}
@@ -320,7 +324,7 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
await SdkLoadService.Ready;
const userKey = new SymmetricCryptoKey(
PureCrypto.decrypt_user_key_with_master_password(
masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString,
masterPasswordUnlockData.masterKeyWrappedUserKey,
password,
masterPasswordUnlockData.salt,
masterPasswordUnlockData.kdf.toSdkConfig(),

View File

@@ -2,8 +2,7 @@ import { Jsonify, Opaque } from "type-fest";
// eslint-disable-next-line no-restricted-imports
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
import { EncString } from "../../crypto/models/enc-string";
import { EncString } from "@bitwarden/sdk-internal";
/**
* 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
*/
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.
@@ -29,7 +28,7 @@ export class MasterPasswordUnlockData {
return {
salt: this.salt,
kdf: this.kdf,
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey.toJSON(),
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey,
};
}
@@ -43,7 +42,7 @@ export class MasterPasswordUnlockData {
obj.kdf.kdfType === KdfType.PBKDF2_SHA256
? PBKDF2KdfConfig.fromJSON(obj.kdf)
: Argon2KdfConfig.fromJSON(obj.kdf),
EncString.fromJSON(obj.masterKeyWrappedUserKey) as MasterKeyWrappedUserKey,
obj.masterKeyWrappedUserKey as MasterKeyWrappedUserKey,
);
}
}