mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-22611] Require userid for masterKey methods on the key service (#15663)
* Require userId on targeted methods. * update method consumers * unit tests
This commit is contained in:
@@ -428,7 +428,8 @@ export class LoginCommand {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const request = new PasswordRequest();
|
const request = new PasswordRequest();
|
||||||
request.masterPasswordHash = await this.keyService.hashMasterKey(currentPassword, null);
|
const masterKey = await this.keyService.getOrDeriveMasterKey(currentPassword, userId);
|
||||||
|
request.masterPasswordHash = await this.keyService.hashMasterKey(currentPassword, masterKey);
|
||||||
request.masterPasswordHint = hint;
|
request.masterPasswordHint = hint;
|
||||||
request.newMasterPasswordHash = newPasswordHash;
|
request.newMasterPasswordHash = newPasswordHash;
|
||||||
request.key = newUserKey[1].encryptedString;
|
request.key = newUserKey[1].encryptedString;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ describe("ChangeEmailComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
keyService.getOrDeriveMasterKey
|
keyService.getOrDeriveMasterKey
|
||||||
.calledWith("password", "UserId")
|
.calledWith("password", "UserId" as UserId)
|
||||||
.mockResolvedValue("getOrDeriveMasterKey" as any);
|
.mockResolvedValue("getOrDeriveMasterKey" as any);
|
||||||
keyService.hashMasterKey
|
keyService.hashMasterKey
|
||||||
.calledWith("password", "getOrDeriveMasterKey" as any)
|
.calledWith("password", "getOrDeriveMasterKey" as any)
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
|
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||||
import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management";
|
import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ export class ChangeKdfConfirmationComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private keyService: KeyService,
|
private keyService: KeyService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
|
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
|
||||||
@@ -58,6 +56,10 @@ export class ChangeKdfConfirmationComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async makeKeyAndSaveAsync() {
|
private async makeKeyAndSaveAsync() {
|
||||||
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
if (activeAccount == null) {
|
||||||
|
throw new Error("No active account found.");
|
||||||
|
}
|
||||||
const masterPassword = this.form.value.masterPassword;
|
const masterPassword = this.form.value.masterPassword;
|
||||||
|
|
||||||
// Ensure the KDF config is valid.
|
// Ensure the KDF config is valid.
|
||||||
@@ -70,13 +72,14 @@ export class ChangeKdfConfirmationComponent {
|
|||||||
request.kdfMemory = this.kdfConfig.memory;
|
request.kdfMemory = this.kdfConfig.memory;
|
||||||
request.kdfParallelism = this.kdfConfig.parallelism;
|
request.kdfParallelism = this.kdfConfig.parallelism;
|
||||||
}
|
}
|
||||||
const masterKey = await this.keyService.getOrDeriveMasterKey(masterPassword);
|
const masterKey = await this.keyService.getOrDeriveMasterKey(masterPassword, activeAccount.id);
|
||||||
request.masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
|
request.masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
|
||||||
const email = await firstValueFrom(
|
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newMasterKey = await this.keyService.makeMasterKey(masterPassword, email, this.kdfConfig);
|
const newMasterKey = await this.keyService.makeMasterKey(
|
||||||
|
masterPassword,
|
||||||
|
activeAccount.email,
|
||||||
|
this.kdfConfig,
|
||||||
|
);
|
||||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||||
masterPassword,
|
masterPassword,
|
||||||
newMasterKey,
|
newMasterKey,
|
||||||
|
|||||||
@@ -163,11 +163,14 @@ export abstract class KeyService {
|
|||||||
*/
|
*/
|
||||||
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId: string): Promise<void>;
|
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId: string): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* @throws Error when userId is null and no active user
|
* Retrieves the user's master key if it is in state, or derives it from the provided password
|
||||||
* @param password The user's master password that will be used to derive a master key if one isn't found
|
* @param password The user's master password that will be used to derive a master key if one isn't found
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
|
* @throws Error when userId is null/undefined.
|
||||||
|
* @throws Error when email or Kdf configuration cannot be found for the user.
|
||||||
|
* @returns The user's master key if it exists, or a newly derived master key.
|
||||||
*/
|
*/
|
||||||
abstract getOrDeriveMasterKey(password: string, userId?: string): Promise<MasterKey>;
|
abstract getOrDeriveMasterKey(password: string, userId: UserId): Promise<MasterKey>;
|
||||||
/**
|
/**
|
||||||
* Generates a master key from the provided password
|
* Generates a master key from the provided password
|
||||||
* @param password The user's master password
|
* @param password The user's master password
|
||||||
@@ -175,7 +178,7 @@ export abstract class KeyService {
|
|||||||
* @param KdfConfig The user's key derivation function configuration
|
* @param KdfConfig The user's key derivation function configuration
|
||||||
* @returns A master key derived from the provided password
|
* @returns A master key derived from the provided password
|
||||||
*/
|
*/
|
||||||
abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey>;
|
abstract makeMasterKey(password: string, email: string, kdfConfig: KdfConfig): Promise<MasterKey>;
|
||||||
/**
|
/**
|
||||||
* Encrypts the existing (or provided) user key with the
|
* Encrypts the existing (or provided) user key with the
|
||||||
* provided master key
|
* provided master key
|
||||||
@@ -191,24 +194,25 @@ export abstract class KeyService {
|
|||||||
* Creates a master password hash from the user's master password. Can
|
* Creates a master password hash from the user's master password. Can
|
||||||
* be used for local authentication or for server authentication depending
|
* be used for local authentication or for server authentication depending
|
||||||
* on the hashPurpose provided.
|
* on the hashPurpose provided.
|
||||||
* @throws Error when password is null or key is null and no active user or active user have no master key
|
|
||||||
* @param password The user's master password
|
* @param password The user's master password
|
||||||
* @param key The user's master key or active's user master key.
|
* @param key The user's master key or active's user master key.
|
||||||
* @param hashPurpose The iterations to use for the hash
|
* @param hashPurpose The iterations to use for the hash. Defaults to {@link HashPurpose.ServerAuthorization}.
|
||||||
|
* @throws Error when password is null/undefined or key is null/undefined.
|
||||||
* @returns The user's master password hash
|
* @returns The user's master password hash
|
||||||
*/
|
*/
|
||||||
abstract hashMasterKey(
|
abstract hashMasterKey(
|
||||||
password: string,
|
password: string,
|
||||||
key: MasterKey | null,
|
key: MasterKey,
|
||||||
hashPurpose?: HashPurpose,
|
hashPurpose?: HashPurpose,
|
||||||
): Promise<string>;
|
): Promise<string>;
|
||||||
/**
|
/**
|
||||||
* Compares the provided master password to the stored password hash.
|
* Compares the provided master password to the stored password hash.
|
||||||
* @param masterPassword The user's master password
|
* @param masterPassword The user's master password
|
||||||
* @param key The user's master key
|
* @param masterKey The user's master key
|
||||||
* @param userId The id of the user to do the operation for.
|
* @param userId The id of the user to do the operation for.
|
||||||
* @returns True if the provided master password matches either the stored
|
* @throws Error when master key is null/undefined.
|
||||||
* key hash or the server key hash
|
* @returns True if the derived master password hash matches the stored
|
||||||
|
* key hash, false otherwise.
|
||||||
*/
|
*/
|
||||||
abstract compareKeyHash(
|
abstract compareKeyHash(
|
||||||
masterPassword: string,
|
masterPassword: string,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/ke
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
@@ -47,6 +47,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
|||||||
import { KdfConfigService } from "./abstractions/kdf-config.service";
|
import { KdfConfigService } from "./abstractions/kdf-config.service";
|
||||||
import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service";
|
import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service";
|
||||||
import { DefaultKeyService } from "./key.service";
|
import { DefaultKeyService } from "./key.service";
|
||||||
|
import { KdfConfig } from "./models/kdf-config";
|
||||||
|
|
||||||
describe("keyService", () => {
|
describe("keyService", () => {
|
||||||
let keyService: DefaultKeyService;
|
let keyService: DefaultKeyService;
|
||||||
@@ -817,55 +818,160 @@ describe("keyService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getOrDeriveMasterKey", () => {
|
describe("getOrDeriveMasterKey", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
masterPasswordService.masterKeySubject.next(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([null as unknown as UserId, undefined as unknown as UserId])(
|
||||||
|
"throws when the provided userId is %s",
|
||||||
|
async (userId) => {
|
||||||
|
await expect(keyService.getOrDeriveMasterKey("password", userId)).rejects.toThrow(
|
||||||
|
"User ID is required.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("returns the master key if it is already available", async () => {
|
it("returns the master key if it is already available", async () => {
|
||||||
const getMasterKey = jest
|
const masterKey = makeSymmetricCryptoKey(32) as MasterKey;
|
||||||
.spyOn(masterPasswordService, "masterKey$")
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
.mockReturnValue(of("masterKey" as any));
|
|
||||||
|
|
||||||
const result = await keyService.getOrDeriveMasterKey("password", mockUserId);
|
const result = await keyService.getOrDeriveMasterKey("password", mockUserId);
|
||||||
|
|
||||||
expect(getMasterKey).toHaveBeenCalledWith(mockUserId);
|
expect(kdfConfigService.getKdfConfig$).not.toHaveBeenCalledWith(mockUserId);
|
||||||
expect(result).toEqual("masterKey");
|
expect(result).toEqual(masterKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("derives the master key if it is not available", async () => {
|
it("throws an error if user's email is not available", async () => {
|
||||||
const getMasterKey = jest
|
accountService.accounts$ = of({});
|
||||||
.spyOn(masterPasswordService, "masterKey$")
|
|
||||||
.mockReturnValue(of(null as any));
|
|
||||||
|
|
||||||
const deriveKeyFromPassword = jest
|
await expect(keyService.getOrDeriveMasterKey("password", mockUserId)).rejects.toThrow(
|
||||||
.spyOn(keyGenerationService, "deriveKeyFromPassword")
|
"No email found for user " + mockUserId,
|
||||||
.mockResolvedValue("mockMasterKey" as any);
|
);
|
||||||
|
expect(kdfConfigService.getKdfConfig$).not.toHaveBeenCalled();
|
||||||
kdfConfigService.getKdfConfig$.mockReturnValue(of("mockKdfConfig" as any));
|
|
||||||
|
|
||||||
const result = await keyService.getOrDeriveMasterKey("password", mockUserId);
|
|
||||||
|
|
||||||
expect(getMasterKey).toHaveBeenCalledWith(mockUserId);
|
|
||||||
expect(deriveKeyFromPassword).toHaveBeenCalledWith("password", "email", "mockKdfConfig");
|
|
||||||
expect(result).toEqual("mockMasterKey");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws an error if no user is found", async () => {
|
|
||||||
accountService.activeAccountSubject.next(null);
|
|
||||||
|
|
||||||
await expect(keyService.getOrDeriveMasterKey("password")).rejects.toThrow("No user found");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error if no kdf config is found", async () => {
|
it("throws an error if no kdf config is found", async () => {
|
||||||
jest.spyOn(masterPasswordService, "masterKey$").mockReturnValue(of(null as any));
|
|
||||||
kdfConfigService.getKdfConfig$.mockReturnValue(of(null));
|
kdfConfigService.getKdfConfig$.mockReturnValue(of(null));
|
||||||
|
|
||||||
await expect(keyService.getOrDeriveMasterKey("password", mockUserId)).rejects.toThrow(
|
await expect(keyService.getOrDeriveMasterKey("password", mockUserId)).rejects.toThrow(
|
||||||
"No kdf found for user",
|
"No kdf found for user",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives the master key if it is not available", async () => {
|
||||||
|
keyGenerationService.deriveKeyFromPassword.mockReturnValue("mockMasterKey" as any);
|
||||||
|
kdfConfigService.getKdfConfig$.mockReturnValue(of("mockKdfConfig" as any));
|
||||||
|
|
||||||
|
const result = await keyService.getOrDeriveMasterKey("password", mockUserId);
|
||||||
|
|
||||||
|
expect(kdfConfigService.getKdfConfig$).toHaveBeenCalledWith(mockUserId);
|
||||||
|
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
|
||||||
|
"password",
|
||||||
|
"email",
|
||||||
|
"mockKdfConfig",
|
||||||
|
);
|
||||||
|
expect(result).toEqual("mockMasterKey");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("makeMasterKey", () => {
|
||||||
|
const password = "testPassword";
|
||||||
|
let email = "test@example.com";
|
||||||
|
const masterKey = makeSymmetricCryptoKey(32) as MasterKey;
|
||||||
|
const kdfConfig = mock<KdfConfig>();
|
||||||
|
|
||||||
|
it("derives a master key from password and email", async () => {
|
||||||
|
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
|
||||||
|
|
||||||
|
const result = await keyService.makeMasterKey(password, email, kdfConfig);
|
||||||
|
|
||||||
|
expect(result).toEqual(masterKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims and lowercases the email for key generation call", async () => {
|
||||||
|
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
|
||||||
|
email = "TEST@EXAMPLE.COM";
|
||||||
|
|
||||||
|
await keyService.makeMasterKey(password, email, kdfConfig);
|
||||||
|
|
||||||
|
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
|
||||||
|
password,
|
||||||
|
email.trim().toLowerCase(),
|
||||||
|
kdfConfig,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log the time taken to derive the master key", async () => {
|
||||||
|
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
|
||||||
|
jest.spyOn(Date.prototype, "getTime").mockReturnValueOnce(1000).mockReturnValueOnce(1500);
|
||||||
|
|
||||||
|
await keyService.makeMasterKey(password, email, kdfConfig);
|
||||||
|
|
||||||
|
expect(logService.info).toHaveBeenCalledWith("[KeyService] Deriving master key took 500ms");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hashMasterKey", () => {
|
||||||
|
const password = "testPassword";
|
||||||
|
const masterKey = makeSymmetricCryptoKey(32) as MasterKey;
|
||||||
|
|
||||||
|
test.each([null as unknown as string, undefined as unknown as string])(
|
||||||
|
"throws when the provided password is %s",
|
||||||
|
async (password) => {
|
||||||
|
await expect(keyService.hashMasterKey(password, masterKey)).rejects.toThrow(
|
||||||
|
"password is required.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each([null as unknown as MasterKey, undefined as unknown as MasterKey])(
|
||||||
|
"throws when the provided key is %s",
|
||||||
|
async (key) => {
|
||||||
|
await expect(keyService.hashMasterKey("password", key)).rejects.toThrow("key is required.");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("hashes master key with default iterations when no hashPurpose is provided", async () => {
|
||||||
|
const mockReturnedHashB64 = "bXlfaGFzaA==";
|
||||||
|
cryptoFunctionService.pbkdf2.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHashB64));
|
||||||
|
|
||||||
|
const result = await keyService.hashMasterKey(password, masterKey);
|
||||||
|
|
||||||
|
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith(
|
||||||
|
masterKey.inner().encryptionKey,
|
||||||
|
password,
|
||||||
|
"sha256",
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
expect(result).toBe(mockReturnedHashB64);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[2, HashPurpose.LocalAuthorization],
|
||||||
|
[1, HashPurpose.ServerAuthorization],
|
||||||
|
])(
|
||||||
|
"hashes master key with %s iterations when hashPurpose is %s",
|
||||||
|
async (expectedIterations, hashPurpose) => {
|
||||||
|
const mockReturnedHashB64 = "bXlfaGFzaA==";
|
||||||
|
cryptoFunctionService.pbkdf2.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHashB64));
|
||||||
|
|
||||||
|
const result = await keyService.hashMasterKey(password, masterKey, hashPurpose);
|
||||||
|
|
||||||
|
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith(
|
||||||
|
masterKey.inner().encryptionKey,
|
||||||
|
password,
|
||||||
|
"sha256",
|
||||||
|
expectedIterations,
|
||||||
|
);
|
||||||
|
expect(result).toBe(mockReturnedHashB64);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("compareKeyHash", () => {
|
describe("compareKeyHash", () => {
|
||||||
type TestCase = {
|
type TestCase = {
|
||||||
masterKey: MasterKey;
|
masterKey: MasterKey;
|
||||||
masterPassword: string | null;
|
masterPassword: string;
|
||||||
storedMasterKeyHash: string | null;
|
storedMasterKeyHash: string | null;
|
||||||
mockReturnedHash: string;
|
mockReturnedHash: string;
|
||||||
expectedToMatch: boolean;
|
expectedToMatch: boolean;
|
||||||
@@ -873,26 +979,33 @@ describe("keyService", () => {
|
|||||||
|
|
||||||
const data: TestCase[] = [
|
const data: TestCase[] = [
|
||||||
{
|
{
|
||||||
masterKey: makeSymmetricCryptoKey(64),
|
masterKey: makeSymmetricCryptoKey(32),
|
||||||
masterPassword: "my_master_password",
|
masterPassword: "my_master_password",
|
||||||
storedMasterKeyHash: "bXlfaGFzaA==",
|
storedMasterKeyHash: "bXlfaGFzaA==",
|
||||||
mockReturnedHash: "bXlfaGFzaA==",
|
mockReturnedHash: "bXlfaGFzaA==",
|
||||||
expectedToMatch: true,
|
expectedToMatch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
masterKey: makeSymmetricCryptoKey(64),
|
masterKey: makeSymmetricCryptoKey(32),
|
||||||
masterPassword: null,
|
masterPassword: null as unknown as string,
|
||||||
storedMasterKeyHash: "bXlfaGFzaA==",
|
storedMasterKeyHash: "bXlfaGFzaA==",
|
||||||
mockReturnedHash: "bXlfaGFzaA==",
|
mockReturnedHash: "bXlfaGFzaA==",
|
||||||
expectedToMatch: false,
|
expectedToMatch: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
masterKey: makeSymmetricCryptoKey(64),
|
masterKey: makeSymmetricCryptoKey(32),
|
||||||
masterPassword: null,
|
masterPassword: null as unknown as string,
|
||||||
storedMasterKeyHash: null,
|
storedMasterKeyHash: null,
|
||||||
mockReturnedHash: "bXlfaGFzaA==",
|
mockReturnedHash: "bXlfaGFzaA==",
|
||||||
expectedToMatch: false,
|
expectedToMatch: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
masterKey: makeSymmetricCryptoKey(32),
|
||||||
|
masterPassword: "my_master_password",
|
||||||
|
storedMasterKeyHash: "bXlfaGFzaA==",
|
||||||
|
mockReturnedHash: "zxccbXlfaGFzaA==",
|
||||||
|
expectedToMatch: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(data)(
|
it.each(data)(
|
||||||
@@ -907,7 +1020,7 @@ describe("keyService", () => {
|
|||||||
masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash);
|
masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash);
|
||||||
|
|
||||||
cryptoFunctionService.pbkdf2
|
cryptoFunctionService.pbkdf2
|
||||||
.calledWith(masterKey.inner().encryptionKey, masterPassword as string, "sha256", 2)
|
.calledWith(masterKey.inner().encryptionKey, masterPassword, "sha256", 2)
|
||||||
.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash));
|
.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash));
|
||||||
|
|
||||||
const actualDidMatch = await keyService.compareKeyHash(
|
const actualDidMatch = await keyService.compareKeyHash(
|
||||||
@@ -919,6 +1032,38 @@ describe("keyService", () => {
|
|||||||
expect(actualDidMatch).toBe(expectedToMatch);
|
expect(actualDidMatch).toBe(expectedToMatch);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test.each([null as unknown as MasterKey, undefined as unknown as MasterKey])(
|
||||||
|
"throws an error if masterKey is %s",
|
||||||
|
async (masterKey) => {
|
||||||
|
await expect(
|
||||||
|
keyService.compareKeyHash("my_master_password", masterKey, mockUserId),
|
||||||
|
).rejects.toThrow("'masterKey' is required to be non-null.");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each([null as unknown as string, undefined as unknown as string])(
|
||||||
|
"returns false when masterPassword is %s",
|
||||||
|
async (masterPassword) => {
|
||||||
|
const result = await keyService.compareKeyHash(
|
||||||
|
masterPassword,
|
||||||
|
makeSymmetricCryptoKey(32),
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("returns false when storedMasterKeyHash is null", async () => {
|
||||||
|
masterPasswordService.masterKeyHashSubject.next(null);
|
||||||
|
|
||||||
|
const result = await keyService.compareKeyHash(
|
||||||
|
"my_master_password",
|
||||||
|
makeSymmetricCryptoKey(32),
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("userPrivateKey$", () => {
|
describe("userPrivateKey$", () => {
|
||||||
|
|||||||
@@ -259,28 +259,28 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move to MasterPasswordService
|
async getOrDeriveMasterKey(password: string, userId: UserId): Promise<MasterKey> {
|
||||||
async getOrDeriveMasterKey(password: string, userId?: UserId) {
|
if (userId == null) {
|
||||||
const [resolvedUserId, email] = await firstValueFrom(
|
throw new Error("User ID is required.");
|
||||||
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
|
}
|
||||||
map(([activeAccount, accounts]) => {
|
|
||||||
userId ??= activeAccount?.id;
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (userId == null || accounts[userId] == null) {
|
|
||||||
throw new Error("No user found");
|
|
||||||
}
|
|
||||||
return [userId, accounts[userId].email];
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
|
|
||||||
if (masterKey != null) {
|
if (masterKey != null) {
|
||||||
return masterKey;
|
return masterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kdf = await firstValueFrom(this.kdfConfigService.getKdfConfig$(resolvedUserId));
|
const email = await firstValueFrom(
|
||||||
if (kdf == null) {
|
this.accountService.accounts$.pipe(map((accounts) => accounts[userId]?.email)),
|
||||||
throw new Error("No kdf found for user");
|
);
|
||||||
|
if (email == null) {
|
||||||
|
throw new Error("No email found for user " + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kdf = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||||
|
if (kdf == null) {
|
||||||
|
throw new Error("No kdf found for user " + userId);
|
||||||
|
}
|
||||||
|
|
||||||
return await this.makeMasterKey(password, email, kdf);
|
return await this.makeMasterKey(password, email, kdf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,14 +289,14 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
|
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
|
||||||
* TODO: Move to MasterPasswordService
|
|
||||||
*/
|
*/
|
||||||
async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey> {
|
async makeMasterKey(password: string, email: string, kdfConfig: KdfConfig): Promise<MasterKey> {
|
||||||
const start = new Date().getTime();
|
const start = new Date().getTime();
|
||||||
|
email = email.trim().toLowerCase();
|
||||||
const masterKey = (await this.keyGenerationService.deriveKeyFromPassword(
|
const masterKey = (await this.keyGenerationService.deriveKeyFromPassword(
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
KdfConfig,
|
kdfConfig,
|
||||||
)) as MasterKey;
|
)) as MasterKey;
|
||||||
const end = new Date().getTime();
|
const end = new Date().getTime();
|
||||||
this.logService.info(`[KeyService] Deriving master key took ${end - start}ms`);
|
this.logService.info(`[KeyService] Deriving master key took ${end - start}ms`);
|
||||||
@@ -312,23 +312,16 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
return await this.buildProtectedSymmetricKey(masterKey, userKey);
|
return await this.buildProtectedSymmetricKey(masterKey, userKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to MasterPasswordService
|
|
||||||
async hashMasterKey(
|
async hashMasterKey(
|
||||||
password: string,
|
password: string,
|
||||||
key: MasterKey | null,
|
key: MasterKey,
|
||||||
hashPurpose?: HashPurpose,
|
hashPurpose?: HashPurpose,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (key == null) {
|
if (password == null) {
|
||||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
throw new Error("password is required.");
|
||||||
if (userId == null) {
|
|
||||||
throw new Error("No active user found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
key = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
|
||||||
}
|
}
|
||||||
|
if (key == null) {
|
||||||
if (password == null || key == null) {
|
throw new Error("key is required.");
|
||||||
throw new Error("Invalid parameters.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
|
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
|
||||||
@@ -341,9 +334,8 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
return Utils.fromBufferToB64(hash);
|
return Utils.fromBufferToB64(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to MasterPasswordService
|
|
||||||
async compareKeyHash(
|
async compareKeyHash(
|
||||||
masterPassword: string | null,
|
masterPassword: string,
|
||||||
masterKey: MasterKey,
|
masterKey: MasterKey,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
|||||||
Reference in New Issue
Block a user