1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-08 19:18:02 +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 { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { EncString } from "../models/domain/enc-string";
import {
MasterKey,
PinKey,
@ -23,6 +24,8 @@ describe("cryptoService", () => {
const logService = mock<LogService>();
const stateService = mock<StateService>();
const mockUserId = "mock user id";
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
@ -43,7 +46,41 @@ describe("cryptoService", () => {
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 mockMasterKey: MasterKey;
let stateSvcGetUserKey: jest.SpyInstance;
@ -58,30 +95,29 @@ describe("cryptoService", () => {
stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey");
});
it("returns the user key if available", async () => {
it("returns the User Key if available", async () => {
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(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);
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
const encryptionKey = await cryptoService.getUserKeyWithLegacySupport();
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetMasterKey).toHaveBeenCalled();
expect(encryptionKey).toEqual(mockMasterKey);
expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(userKey).toEqual(mockMasterKey);
});
});
describe("setUserKey", () => {
const mockUserId = "example user id";
let mockUserKey: UserKey;
beforeEach(() => {
@ -89,25 +125,87 @@ describe("cryptoService", () => {
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
it("saves an Auto key if needed", async () => {
stateService.getVaultTimeout.mockResolvedValue(null);
describe("Auto Key refresh", () => {
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).not.toHaveBeenCalledWith(null, { userId: mockUserId });
expect(stateService.setUserKeyAuto).toHaveBeenCalledWith(mockUserKey.keyB64, {
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 () => {
stateService.getUserKeyPinEphemeral.mockResolvedValue(null);
const cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
cryptoSvcMakePinKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(64).buffer) as PinKey
);
describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance;
const protectedPin =
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
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);
if (storePin) {
await this.storePinKey(key);
await this.storePinKey(key, userId);
// We can't always clear deprecated keys because the pin is only
// migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
@ -727,23 +727,23 @@ export class CryptoService implements CryptoServiceAbstraction {
* ephemeral version.
* @param key The user key
*/
protected async storePinKey(key: UserKey) {
protected async storePinKey(key: UserKey, userId?: string) {
const pin = await this.encryptService.decryptToUtf8(
new EncString(await this.stateService.getProtectedPin()),
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
key
);
const pinKey = await this.makePinKey(
pin,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
await this.stateService.getEmail({ userId: userId }),
await this.stateService.getKdfType({ userId: userId }),
await this.stateService.getKdfConfig({ userId: userId })
);
const encPin = await this.encryptService.encrypt(key.key, pinKey);
if ((await this.stateService.getUserKeyPin()) != null) {
await this.stateService.setUserKeyPin(encPin);
await this.stateService.setUserKeyPin(encPin, { userId: userId });
} 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",
spy: () => makeDeviceKeySpy,