Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ describe("UserVerificationService", () => {
});

it("returns if verification is successful", async () => {
cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(true);
cryptoService.compareKeyHash.mockResolvedValueOnce(true);

const result = await sut.verifyUserByMasterPassword(
{
Expand All @@ -227,7 +227,7 @@ describe("UserVerificationService", () => {
"email",
);

expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled();
expect(cryptoService.compareKeyHash).toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
"localHash",
mockUserId,
Expand All @@ -240,7 +240,7 @@ describe("UserVerificationService", () => {
});

it("throws if verification fails", async () => {
cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(false);
cryptoService.compareKeyHash.mockResolvedValueOnce(false);

await expect(
sut.verifyUserByMasterPassword(
Expand All @@ -253,7 +253,7 @@ describe("UserVerificationService", () => {
),
).rejects.toThrow("Invalid master password");

expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled();
expect(cryptoService.compareKeyHash).toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
});
Expand Down Expand Up @@ -285,7 +285,7 @@ describe("UserVerificationService", () => {
"email",
);

expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled();
expect(cryptoService.compareKeyHash).not.toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
"localHash",
mockUserId,
Expand Down Expand Up @@ -318,7 +318,7 @@ describe("UserVerificationService", () => {
),
).rejects.toThrow("Invalid master password");

expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled();
expect(cryptoService.compareKeyHash).not.toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
let policyOptions: MasterPasswordPolicyResponse | null;
// Client-side verification
if (await this.hasMasterPasswordAndMasterKeyHash(userId)) {
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
const passwordValid = await this.cryptoService.compareKeyHash(
verification.secret,
masterKey,
userId,
);
if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
Expand Down
10 changes: 7 additions & 3 deletions libs/common/src/platform/abstractions/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,18 @@ export abstract class CryptoService {
hashPurpose?: HashPurpose,
): Promise<string>;
/**
* Compares the provided master password to the stored password hash and server password hash.
* Updates the stored hash if outdated.
* Compares the provided master password to the stored password hash.
* @param masterPassword The user's master password
* @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
* 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
* organization keys currently in memory
Expand Down
59 changes: 59 additions & 0 deletions libs/common/src/platform/services/crypto.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,4 +735,63 @@ describe("cryptoService", () => {
});
});
});

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 cryptoService.compareKeyHash(
masterPassword,
masterKey,
mockUserId,
);

expect(actualDidMatch).toBe(expectedToMatch);
},
);
});
});
49 changes: 27 additions & 22 deletions libs/common/src/platform/services/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,34 +319,39 @@
}

// TODO: move to MasterPasswordService
async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
async compareKeyHash(
masterPassword: string,
masterKey: MasterKey,
userId: UserId,
): Promise<boolean> {
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(
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
const serverKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.ServerAuthorization,
);
if (serverKeyHash != null && storedPasswordHash === serverKeyHash) {
await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
return true;
}
if (storedPasswordHash == null) {
return false;

Check warning on line 339 in libs/common/src/platform/services/crypto.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/crypto.service.ts#L339

Added line #L339 was not covered by tests
}

// 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;

Check warning on line 351 in libs/common/src/platform/services/crypto.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/crypto.service.ts#L351

Added line #L351 was not covered by tests
}

return false;
return true;
}

async setOrgKeys(
Expand Down
13 changes: 12 additions & 1 deletion libs/vault/src/components/password-reprompt.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom, map } from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
Expand Down Expand Up @@ -43,16 +45,25 @@
protected i18nService: I18nService,
protected formBuilder: FormBuilder,
protected dialogRef: DialogRef,
protected accountService: AccountService,

Check warning on line 48 in libs/vault/src/components/password-reprompt.component.ts

View check run for this annotation

Codecov / codecov/patch

libs/vault/src/components/password-reprompt.component.ts#L48

Added line #L48 was not covered by tests
) {}

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.");

Check warning on line 55 in libs/vault/src/components/password-reprompt.component.ts

View check run for this annotation

Codecov / codecov/patch

libs/vault/src/components/password-reprompt.component.ts#L55

Added line #L55 was not covered by tests
}

const storedMasterKey = await this.cryptoService.getOrDeriveMasterKey(
this.formGroup.value.masterPassword,
userId,
);
if (
!(await this.cryptoService.compareAndUpdateKeyHash(
!(await this.cryptoService.compareKeyHash(
this.formGroup.value.masterPassword,
storedMasterKey,
userId,
))
) {
this.platformUtilsService.showToast(
Expand Down
Loading