1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +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

@@ -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(