1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00

[PM-14445] TS strict for Key Management, Keys and Lock component (#13121)

* PM-14445: TS strict for Key Management Biometrics

* formatting

* callbacks not null expectations

* state nullability expectations updates

* unit tests fix

* secure channel naming, explicit null check on messageId

* KM-14445: TS strict for Key Management, Keys and Lock component

* conflicts resolution, new strict check failures

* null simplifications

* migrate legacy encryption when no active user throw error instead of hiding it

* throw instead of return
This commit is contained in:
Maciej Zieniuk
2025-02-20 18:45:37 +01:00
committed by GitHub
parent ca41ecba29
commit 3924bc9c84
29 changed files with 403 additions and 279 deletions

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
@@ -40,7 +38,7 @@ export type CipherDecryptionKeys = {
/**
* A users decrypted organization keys.
*/
orgKeys: Record<OrganizationId, OrgKey>;
orgKeys: Record<OrganizationId, OrgKey> | null;
};
export abstract class KeyService {
@@ -49,7 +47,7 @@ export abstract class KeyService {
* is in a locked or logged out state.
* @param userId The user id of the user to get the {@see UserKey} for.
*/
abstract userKey$(userId: UserId): Observable<UserKey>;
abstract userKey$(userId: UserId): Observable<UserKey | null>;
/**
* Returns the an observable key for the given user id.
*
@@ -62,11 +60,11 @@ export abstract class KeyService {
* any other necessary versions (such as auto, biometrics,
* or pin)
*
* @throws when key is null. Lock the account to clear a key
* @throws Error when key or userId is null. Lock the account to clear a key.
* @param key The user key to set
* @param userId The desired user
*/
abstract setUserKey(key: UserKey, userId?: string): Promise<void>;
abstract setUserKey(key: UserKey, userId: UserId): Promise<void>;
/**
* Sets the provided user keys and stores any other necessary versions
* (such as auto, biometrics, or pin).
@@ -129,7 +127,10 @@ export abstract class KeyService {
* @param userId The desired user
* @returns The user key
*/
abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey>;
abstract getUserKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: string,
): Promise<UserKey | null>;
/**
* Determines whether the user key is available for the given user.
@@ -151,10 +152,11 @@ export abstract class KeyService {
abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean>;
/**
* Generates a new user key
* @param masterKey The user's master key
* @throws Error when master key is null and there is no active user
* @param masterKey The user's master key. When null, grabs master key from active user.
* @returns A new user key and the master key protected version of it
*/
abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>;
abstract makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]>;
/**
* Clears the user's stored version of the user key
* @param keySuffix The desired version of the key to clear
@@ -163,11 +165,13 @@ export abstract class KeyService {
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
/**
* Stores the master key encrypted user key
* @throws Error when userId is null and there is no active user.
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise<void>;
abstract setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void>;
/**
* @throws Error when userId is null and no active user
* @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
*/
@@ -195,14 +199,15 @@ export abstract class KeyService {
* Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending
* 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 key The user's master key
* @param key The user's master key or active's user master key.
* @param hashPurpose The iterations to use for the hash
* @returns The user's master password hash
*/
abstract hashMasterKey(
password: string,
key: MasterKey,
key: MasterKey | null,
hashPurpose?: HashPurpose,
): Promise<string>;
/**
@@ -240,13 +245,14 @@ export abstract class KeyService {
/**
* Returns the organization's symmetric key
* @deprecated Use the observable userOrgKeys$ and `map` to the desired {@link OrgKey} instead
* @throws Error when not active user
* @param orgId The desired organization
* @returns The organization's symmetric key
*/
abstract getOrgKey(orgId: string): Promise<OrgKey>;
abstract getOrgKey(orgId: string): Promise<OrgKey | null>;
/**
* Uses the org key to derive a new symmetric key for encrypting data
* @param orgKey The organization's symmetric key
* @param key The organization's symmetric key
*/
abstract makeDataEncKey<T extends UserKey | OrgKey>(
key: T,
@@ -259,13 +265,17 @@ export abstract class KeyService {
*/
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
/**
*
* @throws Error when providerId is null or no active user
* @param providerId The desired provider
* @returns The provider's symmetric key
*/
abstract getProviderKey(providerId: string): Promise<ProviderKey>;
abstract getProviderKey(providerId: string): Promise<ProviderKey | null>;
/**
* Creates a new organization key and encrypts it with the user's public key.
* This method can also return Provider keys for creating new Provider users.
*
* @throws Error when no active user or user have no public key
* @returns The new encrypted org key and the decrypted key itself
*/
abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>;
@@ -281,11 +291,11 @@ export abstract class KeyService {
* from storage and stores it in memory
* @returns The user's private key
*
* @throws An error if there is no user currently active.
* @throws Error when no active user
*
* @deprecated Use {@link userPrivateKey$} instead.
*/
abstract getPrivateKey(): Promise<Uint8Array>;
abstract getPrivateKey(): Promise<Uint8Array | null>;
/**
* Gets an observable stream of the given users decrypted private key, will emit null if the user
@@ -294,7 +304,7 @@ export abstract class KeyService {
*
* @param userId The user id of the user to get the data for.
*/
abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey>;
abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null>;
/**
* Gets an observable stream of the given users encrypted private key, will emit null if the user
@@ -305,7 +315,7 @@ export abstract class KeyService {
* @deprecated Temporary function to allow the SDK to be initialized after the login process, it
* will be removed when auth has been migrated to the SDK.
*/
abstract userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString>;
abstract userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null>;
/**
* Gets an observable stream of the given users decrypted private key with legacy support,
@@ -314,10 +324,12 @@ export abstract class KeyService {
*
* @param userId The user id of the user to get the data for.
*/
abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey>;
abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null>;
/**
* Generates a fingerprint phrase for the user based on their public key
*
* @throws Error when publicKey is null and there is no active user, or the active user does not have a public key
* @param fingerprintMaterial Fingerprint material
* @param publicKey The user's public key
* @returns The user's fingerprint phrase
@@ -410,7 +422,7 @@ export abstract class KeyService {
*/
abstract encryptedOrgKeys$(
userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>>;
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData> | null>;
/**
* Gets an observable stream of the users public key. If the user is does not have
@@ -420,7 +432,7 @@ export abstract class KeyService {
*
* @throws If an invalid user id is passed in.
*/
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey>;
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey | null>;
/**
* Validates that a userkey is correct for a given user

View File

@@ -117,40 +117,40 @@ describe("keyService", () => {
});
});
describe.each(["hasUserKey", "hasUserKeyInMemory"])(
`%s`,
(method: "hasUserKey" | "hasUserKeyInMemory") => {
let mockUserKey: UserKey;
describe.each(["hasUserKey", "hasUserKeyInMemory"])(`%s`, (methodName: string) => {
let mockUserKey: UserKey;
let method: (userId?: UserId) => Promise<boolean>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
method =
methodName === "hasUserKey"
? keyService.hasUserKey.bind(keyService)
: keyService.hasUserKeyInMemory.bind(keyService);
});
it.each([true, false])("returns %s if the user key is set", async (hasKey) => {
it.each([true, false])("returns %s if the user key is set", async (hasKey) => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(hasKey ? mockUserKey : null);
expect(await method(mockUserId)).toBe(hasKey);
});
it("returns false when no active userId is set", async () => {
accountService.activeAccountSubject.next(null);
expect(await method()).toBe(false);
});
it.each([true, false])(
"resolves %s for active user id when none is provided",
async (hasKey) => {
stateProvider.activeUserId$ = of(mockUserId);
stateProvider.singleUser
.getFake(mockUserId, USER_KEY)
.nextState(hasKey ? mockUserKey : null);
expect(await keyService[method](mockUserId)).toBe(hasKey);
});
it("returns false when no active userId is set", async () => {
accountService.activeAccountSubject.next(null);
expect(await keyService[method]()).toBe(false);
});
it.each([true, false])(
"resolves %s for active user id when none is provided",
async (hasKey) => {
stateProvider.activeUserId$ = of(mockUserId);
stateProvider.singleUser
.getFake(mockUserId, USER_KEY)
.nextState(hasKey ? mockUserKey : null);
expect(await keyService[method]()).toBe(hasKey);
},
);
},
);
expect(await method()).toBe(hasKey);
},
);
});
describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey;
@@ -263,11 +263,15 @@ describe("keyService", () => {
});
it("throws if key is null", async () => {
await expect(keyService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
await expect(keyService.setUserKey(null as unknown as UserKey, mockUserId)).rejects.toThrow(
"No key provided.",
);
});
it("throws if userId is null", async () => {
await expect(keyService.setUserKey(mockUserKey, null)).rejects.toThrow("No userId provided.");
await expect(keyService.setUserKey(mockUserKey, null as unknown as UserId)).rejects.toThrow(
"No userId provided.",
);
});
describe("Pin Key refresh", () => {
@@ -338,21 +342,21 @@ describe("keyService", () => {
});
it("throws if userKey is null", async () => {
await expect(keyService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow(
"No userKey provided.",
);
await expect(
keyService.setUserKeys(null as unknown as UserKey, mockEncPrivateKey, mockUserId),
).rejects.toThrow("No userKey provided.");
});
it("throws if encPrivateKey is null", async () => {
await expect(keyService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow(
"No encPrivateKey provided.",
);
await expect(
keyService.setUserKeys(mockUserKey, null as unknown as EncryptedString, mockUserId),
).rejects.toThrow("No encPrivateKey provided.");
});
it("throws if userId is null", async () => {
await expect(keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow(
"No userId provided.",
);
await expect(
keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null as unknown as UserId),
).rejects.toThrow("No userId provided.");
});
it("throws if encPrivateKey cannot be decrypted with the userKey", async () => {
@@ -388,7 +392,7 @@ describe("keyService", () => {
let callCount = 0;
stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++));
await keyService.clearKeys(null);
await keyService.clearKeys();
expect(callCount).toBe(1);
// revert to the original state
@@ -402,7 +406,7 @@ describe("keyService", () => {
USER_KEY,
])("key removal", (key: UserKeyDefinition<unknown>) => {
it(`clears ${key.key} for active user when unspecified`, async () => {
await keyService.clearKeys(null);
await keyService.clearKeys();
const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
@@ -426,7 +430,10 @@ describe("keyService", () => {
makeUserKey: boolean;
};
function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] {
function setupKeys({
makeMasterKey,
makeUserKey,
}: SetupKeysParams): [UserKey | null, MasterKey | null] {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
masterPasswordService.masterKeySubject.next(fakeMasterKey);
@@ -449,7 +456,7 @@ describe("keyService", () => {
const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString);
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString!);
// Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
@@ -526,7 +533,7 @@ describe("keyService", () => {
function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.nextState(keys.userKey);
userKeyState.nextState(keys.userKey!);
}
if ("encryptedPrivateKey" in keys) {
@@ -534,7 +541,7 @@ describe("keyService", () => {
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString);
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey!.encryptedString!);
}
if ("orgKeys" in keys) {
@@ -542,7 +549,7 @@ describe("keyService", () => {
mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
orgKeysState.nextState(keys.orgKeys);
orgKeysState.nextState(keys.orgKeys!);
}
if ("providerKeys" in keys) {
@@ -550,7 +557,7 @@ describe("keyService", () => {
mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS,
);
providerKeysState.nextState(keys.providerKeys);
providerKeysState.nextState(keys.providerKeys!);
}
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {
@@ -572,8 +579,8 @@ describe("keyService", () => {
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).toEqual({});
expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys!.orgKeys).toEqual({});
});
it("returns decryption keys when there are org keys", async () => {
@@ -581,18 +588,18 @@ describe("keyService", () => {
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
},
});
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1);
expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull();
const orgKey = decryptionKeys.orgKeys[org1Id];
expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys!.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys!.orgKeys!)).toHaveLength(1);
expect(decryptionKeys!.orgKeys![org1Id]).not.toBeNull();
const orgKey = decryptionKeys!.orgKeys![org1Id];
expect(orgKey.keyB64).toContain("org1Key");
});
@@ -601,7 +608,7 @@ describe("keyService", () => {
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
},
providerKeys: {},
});
@@ -609,11 +616,11 @@ describe("keyService", () => {
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1);
expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull();
const orgKey = decryptionKeys.orgKeys[org1Id];
expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys!.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys!.orgKeys!)).toHaveLength(1);
expect(decryptionKeys!.orgKeys![org1Id]).not.toBeNull();
const orgKey = decryptionKeys!.orgKeys![org1Id];
expect(orgKey.keyB64).toContain("org1Key");
});
@@ -623,30 +630,30 @@ describe("keyService", () => {
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
[org2Id]: {
type: "provider",
key: makeEncString("provider1Key").encryptedString,
key: makeEncString("provider1Key").encryptedString!,
providerId: "provider1",
},
},
providerKeys: {
provider1: makeEncString("provider1Key").encryptedString,
provider1: makeEncString("provider1Key").encryptedString!,
},
});
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(2);
expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys!.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys!.orgKeys!)).toHaveLength(2);
const orgKey = decryptionKeys.orgKeys[org1Id];
const orgKey = decryptionKeys!.orgKeys![org1Id];
expect(orgKey).not.toBeNull();
expect(orgKey.keyB64).toContain("org1Key");
const org2Key = decryptionKeys.orgKeys[org2Id];
const org2Key = decryptionKeys!.orgKeys![org2Id];
expect(org2Key).not.toBeNull();
expect(org2Key.keyB64).toContain("provider1Key");
});
@@ -686,7 +693,7 @@ describe("keyService", () => {
// User has their org keys set
updateKeys({
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
},
});
@@ -741,7 +748,7 @@ describe("keyService", () => {
type TestCase = {
masterKey: MasterKey;
masterPassword: string | null;
storedMasterKeyHash: string;
storedMasterKeyHash: string | null;
mockReturnedHash: string;
expectedToMatch: boolean;
};
@@ -782,7 +789,7 @@ describe("keyService", () => {
masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash);
cryptoFunctionService.pbkdf2
.calledWith(masterKey.key, masterPassword, "sha256", 2)
.calledWith(masterKey.key, masterPassword as string, "sha256", 2)
.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash));
const actualDidMatch = await keyService.compareKeyHash(

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as bigInt from "big-integer";
import {
NEVER,
@@ -88,7 +86,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
);
) as Observable<Record<OrganizationId, OrgKey>>;
}
async setUserKey(key: UserKey, userId: UserId): Promise<void> {
@@ -152,14 +150,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
return await this.validateUserKey(masterKey as unknown as UserKey, userId);
return await this.validateUserKey(masterKey, userId);
}
// TODO: legacy support for user key is no longer needed since we require users to migrate on login
async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
const userKey = await this.getUserKey(userId);
if (userKey) {
@@ -172,16 +176,25 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return masterKey as unknown as UserKey;
}
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
async getUserKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey | null> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey) {
if (!(await this.validateUserKey(userKey, userId))) {
this.logService.warning("Invalid key, throwing away stored keys");
await this.clearAllStoredUserKeys(userId);
}
return userKey;
if (userId == null) {
throw new Error("No active user id found.");
}
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey == null) {
return null;
}
if (!(await this.validateUserKey(userKey, userId))) {
this.logService.warning("Invalid key, throwing away stored keys");
await this.clearAllStoredUserKeys(userId);
}
return userKey;
}
async hasUserKey(userId?: UserId): Promise<boolean> {
@@ -205,9 +218,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
}
async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> {
if (!masterKey) {
async makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]> {
if (masterKey == null) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
}
if (masterKey == null) {
@@ -241,7 +258,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
}
if (keySuffix === KeySuffixOptions.Pin) {
if (keySuffix === KeySuffixOptions.Pin && userId != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
@@ -251,8 +268,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}
}
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> {
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(userKeyMasterKey),
userId,
@@ -265,16 +286,18 @@ export class DefaultKeyService implements KeyServiceAbstraction {
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
map(([activeAccount, accounts]) => {
userId ??= activeAccount?.id;
return [userId, accounts[userId]?.email];
if (userId == null || accounts[userId] == null) {
throw new Error("No user found");
}
return [userId, accounts[userId].email];
}),
),
);
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
return (masterKey ||= await this.makeMasterKey(
password,
email,
await this.kdfConfigService.getKdfConfig(),
));
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
return (
masterKey ||
(await this.makeMasterKey(password, email, await this.kdfConfigService.getKdfConfig()))
);
}
/**
@@ -303,11 +326,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// TODO: move to MasterPasswordService
async hashMasterKey(
password: string,
key: MasterKey,
key: MasterKey | null,
hashPurpose?: HashPurpose,
): Promise<string> {
if (!key) {
if (key == null) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user found.");
}
key = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
}
@@ -322,7 +349,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// TODO: move to MasterPasswordService
async compareKeyHash(
masterPassword: string,
masterPassword: string | null,
masterKey: MasterKey,
userId: UserId,
): Promise<boolean> {
@@ -386,13 +413,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
});
}
async getOrgKey(orgId: OrganizationId): Promise<OrgKey> {
async getOrgKey(orgId: OrganizationId): Promise<OrgKey | null> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("A user must be active to retrieve an org key");
}
const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId));
return orgKeys[orgId];
return orgKeys?.[orgId] ?? null;
}
async makeDataEncKey<T extends OrgKey | UserKey>(
@@ -427,15 +454,19 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}
// TODO: Deprecate in favor of observable
async getProviderKey(providerId: ProviderId): Promise<ProviderKey> {
async getProviderKey(providerId: ProviderId): Promise<ProviderKey | null> {
if (providerId == null) {
return null;
}
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("No active user found.");
}
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
return providerKeys[providerId] ?? null;
return providerKeys?.[providerId] ?? null;
}
private async clearProviderKeys(userId: UserId): Promise<void> {
@@ -450,7 +481,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> {
const shareKey = await this.keyGenerationService.createKey(512);
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user found.");
}
const publicKey = await firstValueFrom(this.userPublicKey$(userId));
if (publicKey == null) {
throw new Error("No public key found.");
}
const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey);
return [encShareKey, shareKey as T];
}
@@ -465,7 +504,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array> {
async getPrivateKey(): Promise<Uint8Array | null> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
@@ -479,7 +518,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
if (publicKey == null) {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
publicKey = await firstValueFrom(this.userPublicKey$(activeUserId));
if (activeUserId == null) {
throw new Error("No active user found.");
}
publicKey = (await firstValueFrom(this.userPublicKey$(activeUserId))) as Uint8Array;
}
if (publicKey === null) {
@@ -510,12 +552,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
* Clears the user's key pair
* @param userId The desired user
*/
private async clearKeyPair(userId: UserId): Promise<void[]> {
if (userId == null) {
// nothing to do
return;
}
private async clearKeyPair(userId: UserId): Promise<void> {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
}
@@ -596,8 +633,8 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}
// ---HELPERS---
async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> {
if (!key) {
async validateUserKey(key: UserKey | MasterKey | null, userId: UserId): Promise<boolean> {
if (key == null) {
return false;
}
@@ -659,10 +696,14 @@ export class DefaultKeyService implements KeyServiceAbstraction {
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
if (privateKey.encryptedString == null) {
throw new Error("Failed to create valid private key.");
}
await this.setUserKey(userKey, activeUserId);
await this.stateProvider
.getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => privateKey.encryptedString);
.update(() => privateKey.encryptedString!);
return {
userKey,
@@ -692,7 +733,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
if (storePin) {
// Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8(
await this.pinService.getUserKeyEncryptedPin(userId),
(await this.pinService.getUserKeyEncryptedPin(userId))!,
key,
);
@@ -718,7 +759,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId) {
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId) {
let shouldStoreKey = false;
switch (keySuffix) {
case KeySuffixOptions.Auto: {
@@ -744,7 +785,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
protected async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey> {
): Promise<UserKey | null> {
if (keySuffix === KeySuffixOptions.Auto) {
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
if (userKey) {
@@ -754,7 +795,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return null;
}
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
protected async clearAllStoredUserKeys(userId: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
@@ -783,7 +824,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
encryptionKey: SymmetricCryptoKey,
newSymKey: Uint8Array,
): Promise<[T, EncString]> {
let protectedSymKey: EncString = null;
let protectedSymKey: EncString;
if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
@@ -803,12 +844,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) {
} else if (keySuffix === KeySuffixOptions.Pin && userId != null) {
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
}
}
userKey$(userId: UserId): Observable<UserKey> {
userKey$(userId: UserId): Observable<UserKey | null> {
return this.stateProvider.getUser(userId, USER_KEY).state$;
}
@@ -822,7 +863,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// Legacy path
return this.masterPasswordService.masterKey$(userId).pipe(
switchMap(async (masterKey) => {
if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) {
if (!(await this.validateUserKey(masterKey, userId))) {
// We don't have a UserKey or a valid MasterKey
return null;
}
@@ -841,7 +882,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
);
}
private async derivePublicKey(privateKey: UserPrivateKey) {
private async derivePublicKey(privateKey: UserPrivateKey | null) {
if (privateKey == null) {
return null;
}
@@ -849,16 +890,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}
userPrivateKey$(userId: UserId): Observable<UserPrivateKey> {
return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey));
userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null> {
return this.userPrivateKeyHelper$(userId, false).pipe(
map((keys) => keys?.userPrivateKey ?? null),
);
}
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString> {
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
}
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey> {
return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey));
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null> {
return this.userPrivateKeyHelper$(userId, true).pipe(
map((keys) => keys?.userPrivateKey ?? null),
);
}
private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) {
@@ -884,7 +929,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
);
}
private async decryptPrivateKey(encryptedPrivateKey: EncryptedString, key: SymmetricCryptoKey) {
private async decryptPrivateKey(
encryptedPrivateKey: EncryptedString | null,
key: SymmetricCryptoKey,
) {
if (encryptedPrivateKey == null) {
return null;
}
@@ -916,7 +964,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
private providerKeysHelper$(
userId: UserId,
userPrivateKey: UserPrivateKey,
): Observable<Record<ProviderId, ProviderKey>> {
): Observable<Record<ProviderId, ProviderKey> | null> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe(
// Convert each value in the record to it's own decryption observable
convertValues(async (_, value) => {
@@ -943,12 +991,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}
orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> {
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys));
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys ?? null));
}
encryptedOrgKeys$(
userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>> {
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData> | null> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$;
}
@@ -956,7 +1004,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
userId: UserId,
legacySupport: boolean = false,
): Observable<CipherDecryptionKeys | null> {
return this.userPrivateKeyHelper$(userId, legacySupport).pipe(
return this.userPrivateKeyHelper$(userId, legacySupport)?.pipe(
switchMap((userKeys) => {
if (userKeys == null) {
return of(null);
@@ -975,15 +1023,22 @@ export class DefaultKeyService implements KeyServiceAbstraction {
]).pipe(
switchMap(async ([encryptedOrgKeys, providerKeys]) => {
const result: Record<OrganizationId, OrgKey> = {};
for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) {
encryptedOrgKeys = encryptedOrgKeys ?? {};
for (const orgId of Object.keys(encryptedOrgKeys) as OrganizationId[]) {
if (result[orgId] != null) {
continue;
}
const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]);
if (encrypted == null) {
continue;
}
let decrypted: OrgKey;
if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) {
if (providerKeys == null) {
throw new Error("No provider keys found.");
}
decrypted = await encrypted.decrypt(this.encryptService, providerKeys);
} else {
decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey);

View File

@@ -119,6 +119,9 @@ export class DefaultUserAsymmetricKeysRegenerationService
private async regenerateUserAsymmetricKeys(userId: UserId): Promise<void> {
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("User key not found");
}
const makeKeyPairResponse = await firstValueFrom(
this.sdkService.client$.pipe(
map((sdk) => {