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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user