1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-24 12:06:15 +01:00

[PM-13673] Require UserId In CompareHash Method (#11568)

* Require UserId In CompareHash Method

* Throw on null-ish 'masterKey'

* Update Test
This commit is contained in:
Justin Baur 2024-11-04 15:11:59 -05:00 committed by GitHub
parent 008e928d0a
commit f41365ce48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 117 additions and 33 deletions

View File

@ -216,7 +216,7 @@ describe("UserVerificationService", () => {
}); });
it("returns if verification is successful", async () => { it("returns if verification is successful", async () => {
keyService.compareAndUpdateKeyHash.mockResolvedValueOnce(true); keyService.compareKeyHash.mockResolvedValueOnce(true);
const result = await sut.verifyUserByMasterPassword( const result = await sut.verifyUserByMasterPassword(
{ {
@ -227,7 +227,7 @@ describe("UserVerificationService", () => {
"email", "email",
); );
expect(keyService.compareAndUpdateKeyHash).toHaveBeenCalled(); expect(keyService.compareKeyHash).toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
"localHash", "localHash",
mockUserId, mockUserId,
@ -240,7 +240,7 @@ describe("UserVerificationService", () => {
}); });
it("throws if verification fails", async () => { it("throws if verification fails", async () => {
keyService.compareAndUpdateKeyHash.mockResolvedValueOnce(false); keyService.compareKeyHash.mockResolvedValueOnce(false);
await expect( await expect(
sut.verifyUserByMasterPassword( sut.verifyUserByMasterPassword(
@ -253,7 +253,7 @@ describe("UserVerificationService", () => {
), ),
).rejects.toThrow("Invalid master password"); ).rejects.toThrow("Invalid master password");
expect(keyService.compareAndUpdateKeyHash).toHaveBeenCalled(); expect(keyService.compareKeyHash).toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith(); expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith(); expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
}); });
@ -285,7 +285,7 @@ describe("UserVerificationService", () => {
"email", "email",
); );
expect(keyService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); expect(keyService.compareKeyHash).not.toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
"localHash", "localHash",
mockUserId, mockUserId,
@ -318,7 +318,7 @@ describe("UserVerificationService", () => {
), ),
).rejects.toThrow("Invalid master password"); ).rejects.toThrow("Invalid master password");
expect(keyService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); expect(keyService.compareKeyHash).not.toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith(); expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith(); expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
}); });

View File

@ -206,9 +206,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
let policyOptions: MasterPasswordPolicyResponse | null; let policyOptions: MasterPasswordPolicyResponse | null;
// Client-side verification // Client-side verification
if (await this.hasMasterPasswordAndMasterKeyHash(userId)) { if (await this.hasMasterPasswordAndMasterKeyHash(userId)) {
const passwordValid = await this.keyService.compareAndUpdateKeyHash( const passwordValid = await this.keyService.compareKeyHash(
verification.secret, verification.secret,
masterKey, masterKey,
userId,
); );
if (!passwordValid) { if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword")); throw new Error(this.i18nService.t("invalidMasterPassword"));

View File

@ -204,14 +204,18 @@ export abstract class KeyService {
hashPurpose?: HashPurpose, hashPurpose?: HashPurpose,
): Promise<string>; ): Promise<string>;
/** /**
* Compares the provided master password to the stored password hash and server password hash. * Compares the provided master password to the stored password hash.
* Updates the stored hash if outdated.
* @param masterPassword The user's master password * @param masterPassword The user's master password
* @param key The user's master key * @param key The user's master key
* @param userId The id of the user to do the operation for.
* @returns True if the provided master password matches either the stored * @returns True if the provided master password matches either the stored
* key hash or the server key hash * key hash or the server key hash
*/ */
abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean>; abstract compareKeyHash(
masterPassword: string,
masterKey: MasterKey,
userId: UserId,
): Promise<boolean>;
/** /**
* Stores the encrypted organization keys and clears any decrypted * Stores the encrypted organization keys and clears any decrypted
* organization keys currently in memory * organization keys currently in memory

View File

@ -733,4 +733,63 @@ describe("keyService", () => {
}); });
}); });
}); });
describe("compareKeyHash", () => {
type TestCase = {
masterKey: MasterKey;
masterPassword: string | null;
storedMasterKeyHash: string;
mockReturnedHash: string;
expectedToMatch: boolean;
};
const data: TestCase[] = [
{
masterKey: makeSymmetricCryptoKey(64),
masterPassword: "my_master_password",
storedMasterKeyHash: "bXlfaGFzaA==",
mockReturnedHash: "bXlfaGFzaA==",
expectedToMatch: true,
},
{
masterKey: makeSymmetricCryptoKey(64),
masterPassword: null,
storedMasterKeyHash: "bXlfaGFzaA==",
mockReturnedHash: "bXlfaGFzaA==",
expectedToMatch: false,
},
{
masterKey: makeSymmetricCryptoKey(64),
masterPassword: null,
storedMasterKeyHash: null,
mockReturnedHash: "bXlfaGFzaA==",
expectedToMatch: false,
},
];
it.each(data)(
"returns expected match value when calculated hash equals stored hash",
async ({
masterKey,
masterPassword,
storedMasterKeyHash,
mockReturnedHash,
expectedToMatch,
}) => {
masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash);
cryptoFunctionService.pbkdf2
.calledWith(masterKey.key, masterPassword, "sha256", 2)
.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash));
const actualDidMatch = await keyService.compareKeyHash(
masterPassword,
masterKey,
mockUserId,
);
expect(actualDidMatch).toBe(expectedToMatch);
},
);
});
}); });

View File

@ -319,34 +319,43 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
// TODO: move to MasterPasswordService // TODO: move to MasterPasswordService
async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> { async compareKeyHash(
const userId = await firstValueFrom(this.stateProvider.activeUserId$); masterPassword: string,
masterKey: MasterKey,
userId: UserId,
): Promise<boolean> {
if (masterKey == null) {
throw new Error("'masterKey' is required to be non-null.");
}
if (masterPassword == null) {
// If they don't give us a master password, we can't hash it, and therefore
// it will never match what we have stored.
return false;
}
// Retrieve the current password hash
const storedPasswordHash = await firstValueFrom( const storedPasswordHash = await firstValueFrom(
this.masterPasswordService.masterKeyHash$(userId), this.masterPasswordService.masterKeyHash$(userId),
); );
if (masterPassword != null && storedPasswordHash != null) {
const localKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.LocalAuthorization,
);
if (localKeyHash != null && storedPasswordHash === localKeyHash) {
return true;
}
// TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated if (storedPasswordHash == null) {
const serverKeyHash = await this.hashMasterKey( return false;
masterPassword,
masterKey,
HashPurpose.ServerAuthorization,
);
if (serverKeyHash != null && storedPasswordHash === serverKeyHash) {
await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
return true;
}
} }
return false; // Hash the key for local use
const localKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.LocalAuthorization,
);
// Check if the stored hash is already equal to the hash we create locally
if (localKeyHash == null || storedPasswordHash !== localKeyHash) {
return false;
}
return true;
} }
async setOrgKeys( async setOrgKeys(

View File

@ -1,8 +1,10 @@
import { DialogRef } from "@angular/cdk/dialog"; import { DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { import {
@ -43,16 +45,25 @@ export class PasswordRepromptComponent {
protected i18nService: I18nService, protected i18nService: I18nService,
protected formBuilder: FormBuilder, protected formBuilder: FormBuilder,
protected dialogRef: DialogRef, protected dialogRef: DialogRef,
protected accountService: AccountService,
) {} ) {}
submit = async () => { submit = async () => {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
if (userId == null) {
throw new Error("An active user is expected while doing password reprompt.");
}
const storedMasterKey = await this.keyService.getOrDeriveMasterKey( const storedMasterKey = await this.keyService.getOrDeriveMasterKey(
this.formGroup.value.masterPassword, this.formGroup.value.masterPassword,
userId,
); );
if ( if (
!(await this.keyService.compareAndUpdateKeyHash( !(await this.keyService.compareKeyHash(
this.formGroup.value.masterPassword, this.formGroup.value.masterPassword,
storedMasterKey, storedMasterKey,
userId,
)) ))
) { ) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(