1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-24 21:41:33 +01:00

add better tests to crypto service

This commit is contained in:
Jacob Fink 2023-06-28 16:39:18 -04:00
parent b41248cece
commit 7b7fa276be
No known key found for this signature in database
GPG Key ID: C2F7ACF05859D008
4 changed files with 221 additions and 32 deletions

View File

@ -0,0 +1,91 @@
import { mock, mockReset } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { ElectronCryptoService } from "./electron-crypto.service";
import { ElectronStateService } from "./electron-state.service.abstraction";
describe("electronCryptoService", () => {
let electronCryptoService: ElectronCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
const platformUtilService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
const stateService = mock<ElectronStateService>();
const mockUserId = "mock user id";
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
mockReset(platformUtilService);
mockReset(logService);
mockReset(stateService);
electronCryptoService = new ElectronCryptoService(
cryptoFunctionService,
encryptService,
platformUtilService,
logService,
stateService
);
});
it("instantiates", () => {
expect(electronCryptoService).not.toBeFalsy();
});
describe("setUserKey", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64).buffer as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
describe("Biometric Key refresh", () => {
it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
stateService.getBiometricUnlock.mockResolvedValue(true);
platformUtilService.supportsSecureStorage.mockReturnValue(true);
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false);
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(
expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }),
{
userId: mockUserId,
}
);
});
it("clears the Biometric key if getBiometricUnlock is false or the platform does not support secure storage", async () => {
stateService.getBiometricUnlock.mockResolvedValue(true);
platformUtilService.supportsSecureStorage.mockReturnValue(false);
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
it("clears the old deprecated Biometric key whenever a User Key is set", async () => {
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyBiometric).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
});
});

View File

@ -6,6 +6,7 @@ import { EncryptService } from "../abstractions/encrypt.service";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { EncString } from "../models/domain/enc-string";
import { import {
MasterKey, MasterKey,
PinKey, PinKey,
@ -23,6 +24,8 @@ describe("cryptoService", () => {
const logService = mock<LogService>(); const logService = mock<LogService>();
const stateService = mock<StateService>(); const stateService = mock<StateService>();
const mockUserId = "mock user id";
beforeEach(() => { beforeEach(() => {
mockReset(cryptoFunctionService); mockReset(cryptoFunctionService);
mockReset(encryptService); mockReset(encryptService);
@ -43,7 +46,41 @@ describe("cryptoService", () => {
expect(cryptoService).not.toBeFalsy(); expect(cryptoService).not.toBeFalsy();
}); });
describe("getKeyForUserDecryption", () => { describe("getUserKey", () => {
let mockUserKey: UserKey;
let stateSvcGetUserKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64).buffer as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
});
it("returns the User Key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(userKey).toEqual(mockUserKey);
});
it("sets the Auto key if the User Key if not set", async () => {
const autoKeyB64 =
"IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g==";
stateService.getUserKeyAuto.mockResolvedValue(autoKeyB64);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(stateService.setUserKey).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), {
userId: mockUserId,
});
expect(userKey.keyB64).toEqual(autoKeyB64);
});
});
describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey; let mockUserKey: UserKey;
let mockMasterKey: MasterKey; let mockMasterKey: MasterKey;
let stateSvcGetUserKey: jest.SpyInstance; let stateSvcGetUserKey: jest.SpyInstance;
@ -58,30 +95,29 @@ describe("cryptoService", () => {
stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey"); stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey");
}); });
it("returns the user key if available", async () => { it("returns the User Key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey); stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const encryptionKey = await cryptoService.getUserKeyWithLegacySupport(); const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetUserKey).toHaveBeenCalled(); expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(stateSvcGetMasterKey).not.toHaveBeenCalled(); expect(stateSvcGetMasterKey).not.toHaveBeenCalled();
expect(encryptionKey).toEqual(mockUserKey); expect(userKey).toEqual(mockUserKey);
}); });
it("returns the user's master key when symmetric key is not available", async () => { it("returns the user's master key when User Key is not available", async () => {
stateSvcGetUserKey.mockResolvedValue(null); stateSvcGetUserKey.mockResolvedValue(null);
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey); stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
const encryptionKey = await cryptoService.getUserKeyWithLegacySupport(); const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetMasterKey).toHaveBeenCalled(); expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(encryptionKey).toEqual(mockMasterKey); expect(userKey).toEqual(mockMasterKey);
}); });
}); });
describe("setUserKey", () => { describe("setUserKey", () => {
const mockUserId = "example user id";
let mockUserKey: UserKey; let mockUserKey: UserKey;
beforeEach(() => { beforeEach(() => {
@ -89,25 +125,87 @@ describe("cryptoService", () => {
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
}); });
it("saves an Auto key if needed", async () => { describe("Auto Key refresh", () => {
stateService.getVaultTimeout.mockResolvedValue(null); it("sets an Auto key if vault timeout is set to null", async () => {
stateService.getVaultTimeout.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAuto).toHaveBeenCalled(); expect(stateService.setUserKeyAuto).toHaveBeenCalledWith(mockUserKey.keyB64, {
expect(stateService.setUserKeyAuto).not.toHaveBeenCalledWith(null, { userId: mockUserId }); userId: mockUserId,
});
});
it("clears the Auto key if vault timeout is set to anything other than null", async () => {
stateService.getVaultTimeout.mockResolvedValue(10);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAuto).toHaveBeenCalledWith(null, { userId: mockUserId });
});
it("clears the old deprecated Auto key whenever a User Key is set", async () => {
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyAuto).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
}); });
it("saves a Pin key if needed", async () => { describe("Pin Key refresh", () => {
stateService.getUserKeyPinEphemeral.mockResolvedValue(null); let cryptoSvcMakePinKey: jest.SpyInstance;
const cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey"); const protectedPin =
cryptoSvcMakePinKey.mockResolvedValue( "2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
new SymmetricCryptoKey(new Uint8Array(64).buffer) as PinKey let encPin: EncString;
);
await cryptoService.setUserKey(mockUserKey, mockUserId); beforeEach(() => {
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
cryptoSvcMakePinKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(64).buffer) as PinKey
);
encPin = new EncString(
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg="
);
encryptService.encrypt.mockResolvedValue(encPin);
});
expect(stateService.setUserKeyPin).toHaveBeenCalled(); it("sets a UserKeyPin if a ProtectedPin and UserKeyPin is set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getUserKeyPin.mockResolvedValue(
new EncString(
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A="
)
);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyPin).toHaveBeenCalledWith(expect.any(EncString), {
userId: mockUserId,
});
});
it("sets a PinKeyEphemeral if a ProtectedPin is set, but a UserKeyPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getUserKeyPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyPinEphemeral).toHaveBeenCalledWith(expect.any(EncString), {
userId: mockUserId,
});
});
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyPin).toHaveBeenCalledWith(null, { userId: mockUserId });
expect(stateService.setUserKeyPinEphemeral).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
}); });
}); });
}); });

View File

@ -712,7 +712,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId); const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) { if (storePin) {
await this.storePinKey(key); await this.storePinKey(key, userId);
// We can't always clear deprecated keys because the pin is only // We can't always clear deprecated keys because the pin is only
// migrated once used to unlock // migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId); await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
@ -727,23 +727,23 @@ export class CryptoService implements CryptoServiceAbstraction {
* ephemeral version. * ephemeral version.
* @param key The user key * @param key The user key
*/ */
protected async storePinKey(key: UserKey) { protected async storePinKey(key: UserKey, userId?: string) {
const pin = await this.encryptService.decryptToUtf8( const pin = await this.encryptService.decryptToUtf8(
new EncString(await this.stateService.getProtectedPin()), new EncString(await this.stateService.getProtectedPin({ userId: userId })),
key key
); );
const pinKey = await this.makePinKey( const pinKey = await this.makePinKey(
pin, pin,
await this.stateService.getEmail(), await this.stateService.getEmail({ userId: userId }),
await this.stateService.getKdfType(), await this.stateService.getKdfType({ userId: userId }),
await this.stateService.getKdfConfig() await this.stateService.getKdfConfig({ userId: userId })
); );
const encPin = await this.encryptService.encrypt(key.key, pinKey); const encPin = await this.encryptService.encrypt(key.key, pinKey);
if ((await this.stateService.getUserKeyPin()) != null) { if ((await this.stateService.getUserKeyPin()) != null) {
await this.stateService.setUserKeyPin(encPin); await this.stateService.setUserKeyPin(encPin, { userId: userId });
} else { } else {
await this.stateService.setUserKeyPinEphemeral(encPin); await this.stateService.setUserKeyPinEphemeral(encPin, { userId: userId });
} }
} }

View File

@ -268,7 +268,7 @@ describe("deviceCryptoService", () => {
); );
}); });
const methodsToTestForErrorsOrInvalidReturns = [ const methodsToTestForErrorsOrInvalidReturns: any = [
{ {
method: "makeDeviceKey", method: "makeDeviceKey",
spy: () => makeDeviceKeySpy, spy: () => makeDeviceKeySpy,