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:
parent
008e928d0a
commit
f41365ce48
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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"));
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user