1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-17 18:09:17 +00:00

[PM-30633] Add setLegacyMasterKeyFromUnlockData (#18316)

* Add setMasterKeyFromunlockData

* Rename method

* Add comment

* Add tests

* Fix build

* Fix linting

* Add set local master key hash

* Add tests

* Cleanup

* Move function

* Prettier

* Fix tests

* Eslint

* Fix tests
This commit is contained in:
Bernd Schoolmann
2026-01-14 19:06:13 +01:00
committed by GitHub
parent 0a9dc69aa9
commit aec049aa84
4 changed files with 193 additions and 0 deletions

View File

@@ -113,6 +113,23 @@ export abstract class MasterPasswordServiceAbstraction {
* @throws If the user ID is missing.
*/
abstract userHasMasterPassword(userId: UserId): Promise<boolean>;
/**
* Derives a master key from the provided password and master password unlock data,
* then sets it to state for the specified user. This is a temporary backwards compatibility function
* to support existing code that relies on direct master key access.
* Note: This will be removed in https://bitwarden.atlassian.net/browse/PM-30676
*
* @param password The master password.
* @param masterPasswordUnlockData The master password unlock data containing the KDF settings and salt.
* @param userId The user ID.
* @throws If the password, master password unlock data, or user ID is missing.
*/
abstract setLegacyMasterKeyFromUnlockData(
password: string,
masterPasswordUnlockData: MasterPasswordUnlockData,
userId: UserId,
): Promise<void>;
}
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {

View File

@@ -127,4 +127,12 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
masterPasswordUnlockData$(userId: UserId): Observable<MasterPasswordUnlockData | null> {
return this.mock.masterPasswordUnlockData$(userId);
}
setLegacyMasterKeyFromUnlockData(
password: string,
masterPasswordUnlockData: MasterPasswordUnlockData,
userId: UserId,
): Promise<void> {
return this.mock.setLegacyMasterKeyFromUnlockData(password, masterPasswordUnlockData, userId);
}
}

View File

@@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
import { Jsonify } from "type-fest";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
// eslint-disable-next-line no-restricted-imports
import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management";
@@ -415,6 +416,125 @@ describe("MasterPasswordService", () => {
);
});
describe("setLegacyMasterKeyFromUnlockData", () => {
const password = "test-password";
it("derives master key from password and sets it in state", async () => {
const masterKey = makeSymmetricCryptoKey(32, 5) as MasterKey;
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32));
const masterPasswordUnlockData = new MasterPasswordUnlockData(
salt,
kdfPBKDF2,
makeEncString().toSdk() as MasterKeyWrappedUserKey,
);
await sut.setLegacyMasterKeyFromUnlockData(password, masterPasswordUnlockData, userId);
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
password,
masterPasswordUnlockData.salt,
masterPasswordUnlockData.kdf,
);
const state = await firstValueFrom(stateProvider.getUser(userId, MASTER_KEY).state$);
expect(state).toEqual(masterKey);
});
it("works with argon2 kdf config", async () => {
const masterKey = makeSymmetricCryptoKey(32, 6) as MasterKey;
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32));
const masterPasswordUnlockData = new MasterPasswordUnlockData(
salt,
kdfArgon2,
makeEncString().toSdk() as MasterKeyWrappedUserKey,
);
await sut.setLegacyMasterKeyFromUnlockData(password, masterPasswordUnlockData, userId);
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
password,
masterPasswordUnlockData.salt,
masterPasswordUnlockData.kdf,
);
const state = await firstValueFrom(stateProvider.getUser(userId, MASTER_KEY).state$);
expect(state).toEqual(masterKey);
});
it("computes and sets master key hash in state", async () => {
const masterKey = makeSymmetricCryptoKey(32, 7) as MasterKey;
const expectedHashBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
const expectedHashB64 = "AQIDBAUGBwg=";
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
cryptoFunctionService.pbkdf2.mockResolvedValue(expectedHashBytes);
jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(expectedHashB64);
const masterPasswordUnlockData = new MasterPasswordUnlockData(
salt,
kdfPBKDF2,
makeEncString().toSdk() as MasterKeyWrappedUserKey,
);
await sut.setLegacyMasterKeyFromUnlockData(password, masterPasswordUnlockData, userId);
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith(
masterKey.inner().encryptionKey,
password,
"sha256",
HashPurpose.LocalAuthorization,
);
const hashState = await firstValueFrom(sut.masterKeyHash$(userId));
expect(hashState).toEqual(expectedHashB64);
});
it("throws if password is null", async () => {
const masterPasswordUnlockData = new MasterPasswordUnlockData(
salt,
kdfPBKDF2,
makeEncString().toSdk() as MasterKeyWrappedUserKey,
);
await expect(
sut.setLegacyMasterKeyFromUnlockData(
null as unknown as string,
masterPasswordUnlockData,
userId,
),
).rejects.toThrow("password is null or undefined.");
});
it("throws if masterPasswordUnlockData is null", async () => {
await expect(
sut.setLegacyMasterKeyFromUnlockData(
password,
null as unknown as MasterPasswordUnlockData,
userId,
),
).rejects.toThrow("masterPasswordUnlockData is null or undefined.");
});
it("throws if userId is null", async () => {
const masterPasswordUnlockData = new MasterPasswordUnlockData(
salt,
kdfPBKDF2,
makeEncString().toSdk() as MasterKeyWrappedUserKey,
);
await expect(
sut.setLegacyMasterKeyFromUnlockData(
password,
masterPasswordUnlockData,
null as unknown as UserId,
),
).rejects.toThrow("userId is null or undefined.");
});
});
describe("MASTER_PASSWORD_UNLOCK_KEY", () => {
it("has the correct configuration", () => {
expect(MASTER_PASSWORD_UNLOCK_KEY.stateDefinition).toBeDefined();

View File

@@ -5,6 +5,7 @@ import { firstValueFrom, map, Observable } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { assertNonNullish } from "@bitwarden/common/auth/utils";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
// eslint-disable-next-line no-restricted-imports
import { KdfConfig } from "@bitwarden/key-management";
@@ -342,4 +343,51 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
return this.stateProvider.getUser(userId, MASTER_PASSWORD_UNLOCK_KEY).state$;
}
async setLegacyMasterKeyFromUnlockData(
password: string,
masterPasswordUnlockData: MasterPasswordUnlockData,
userId: UserId,
): Promise<void> {
assertNonNullish(password, "password");
assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData");
assertNonNullish(userId, "userId");
const masterKey = (await this.keyGenerationService.deriveKeyFromPassword(
password,
masterPasswordUnlockData.salt,
masterPasswordUnlockData.kdf,
)) as MasterKey;
const localKeyHash = await this.hashMasterKey(
password,
masterKey,
HashPurpose.LocalAuthorization,
);
await this.setMasterKey(masterKey, userId);
await this.setMasterKeyHash(localKeyHash, userId);
}
// Copied from KeyService to avoid circular dependency. This will be dropped together with `setLegacyMatserKeyFromUnlockData`.
private async hashMasterKey(
password: string,
key: MasterKey,
hashPurpose: HashPurpose,
): Promise<string> {
if (password == null) {
throw new Error("password is required.");
}
if (key == null) {
throw new Error("key is required.");
}
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
const hash = await this.cryptoFunctionService.pbkdf2(
key.inner().encryptionKey,
password,
"sha256",
iterations,
);
return Utils.fromBufferToB64(hash);
}
}