1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00
Files
browser/libs/common/src/services/device-crypto.service.spec.ts

322 lines
12 KiB
TypeScript

import { mock, mockReset } from "jest-mock-extended";
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { EncryptionType } from "../enums/encryption-type.enum";
import { AppIdService } from "../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../platform/abstractions/crypto-function.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { EncString } from "../platform/models/domain/enc-string";
import {
SymmetricCryptoKey,
DeviceKey,
UserSymKey,
} from "../platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "../platform/services/crypto.service";
import { CsprngArray } from "../types/csprng";
import { DeviceCryptoService } from "./device-crypto.service.implementation";
describe("deviceCryptoService", () => {
let deviceCryptoService: DeviceCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
const stateService = mock<StateService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
mockReset(stateService);
mockReset(appIdService);
mockReset(devicesApiService);
deviceCryptoService = new DeviceCryptoService(
cryptoFunctionService,
cryptoService,
encryptService,
stateService,
appIdService,
devicesApiService
);
});
it("instantiates", () => {
expect(deviceCryptoService).not.toBeFalsy();
});
describe("Trusted Device Encryption", () => {
const deviceKeyBytesLength = 64;
const userSymKeyBytesLength = 64;
describe("getDeviceKey", () => {
let mockRandomBytes: CsprngArray;
let mockDeviceKey: SymmetricCryptoKey;
let existingDeviceKey: DeviceKey;
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
let makeDeviceKeySpy: jest.SpyInstance;
beforeEach(() => {
mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
existingDeviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
) as DeviceKey;
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
makeDeviceKeySpy = jest.spyOn(deviceCryptoService as any, "makeDeviceKey");
});
it("gets a device key when there is not an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
makeDeviceKeySpy.mockResolvedValue(mockDeviceKey);
const deviceKey = await deviceCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(mockDeviceKey);
});
it("returns the existing device key without creating a new one when there is an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
const deviceKey = await deviceCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(makeDeviceKeySpy).not.toHaveBeenCalled();
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(existingDeviceKey);
});
});
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
const cryptoFuncSvcRandomBytesSpy = jest
.spyOn(cryptoFunctionService, "randomBytes")
.mockResolvedValue(mockRandomBytes);
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
const deviceKey = await (deviceCryptoService as any).makeDeviceKey();
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
});
});
describe("trustDevice", () => {
let mockDeviceKeyRandomBytes: CsprngArray;
let mockDeviceKey: DeviceKey;
let mockUserSymKeyRandomBytes: CsprngArray;
let mockUserSymKey: UserSymKey;
const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [ArrayBuffer, ArrayBuffer];
let mockDevicePrivateKey: ArrayBuffer;
let mockDevicePublicKey: ArrayBuffer;
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
Id: "mockId",
Name: "mockName",
Identifier: "mockIdentifier",
Type: "mockType",
CreationDate: "mockCreationDate",
});
const mockDeviceId = "mockDeviceId";
let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetUserKeyFromMemorySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance;
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
beforeEach(() => {
// Setup all spies and default return values for the happy path
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray;
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes) as UserSymKey;
mockDeviceRsaKeyPair = [
new ArrayBuffer(deviceRsaKeyLength),
new ArrayBuffer(deviceRsaKeyLength),
];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
mockDevicePublicKeyEncryptedUserSymKey = new EncString(
EncryptionType.Rsa2048_OaepSha1_B64,
"mockDevicePublicKeyEncryptedUserSymKey"
);
mockUserSymKeyEncryptedDevicePublicKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockUserSymKeyEncryptedDevicePublicKey"
);
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockDeviceKeyEncryptedDevicePrivateKey"
);
// TypeScript will allow calling private methods if the object is of type 'any'
makeDeviceKeySpy = jest
.spyOn(deviceCryptoService as any, "makeDeviceKey")
.mockResolvedValue(mockDeviceKey);
rsaGenerateKeyPairSpy = jest
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetUserKeyFromMemorySpy = jest
.spyOn(cryptoService, "getUserKeyFromMemory")
.mockResolvedValue(mockUserSymKey);
cryptoSvcRsaEncryptSpy = jest
.spyOn(cryptoService, "rsaEncrypt")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserSymKey);
encryptServiceEncryptSpy = jest
.spyOn(encryptService, "encrypt")
.mockImplementation((plainValue, key) => {
if (plainValue === mockDevicePublicKey && key === mockUserSymKey) {
return Promise.resolve(mockUserSymKeyEncryptedDevicePublicKey);
}
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
}
});
appIdServiceGetAppIdSpy = jest
.spyOn(appIdService, "getAppId")
.mockResolvedValue(mockDeviceId);
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
.mockResolvedValue(mockDeviceResponse);
});
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
const response = await deviceCryptoService.trustDevice();
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetUserKeyFromMemorySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
mockDeviceId,
mockDevicePublicKeyEncryptedUserSymKey.encryptedString,
mockUserSymKeyEncryptedDevicePublicKey.encryptedString,
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
);
expect(response).toBeInstanceOf(DeviceResponse);
expect(response).toEqual(mockDeviceResponse);
});
it("throws specific error if user symmetric key is not found", async () => {
// setup the spy to return null
cryptoSvcGetUserKeyFromMemorySpy.mockResolvedValue(null);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
// reset the spy
cryptoSvcGetUserKeyFromMemorySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetUserKeyFromMemorySpy.mockResolvedValue(undefined);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
});
const methodsToTestForErrorsOrInvalidReturns = [
{
method: "makeDeviceKey",
spy: () => makeDeviceKeySpy,
errorText: "makeDeviceKey error",
},
{
method: "rsaGenerateKeyPair",
spy: () => rsaGenerateKeyPairSpy,
errorText: "rsaGenerateKeyPair error",
},
{
method: "getUserKeyFromMemory",
spy: () => cryptoSvcGetUserKeyFromMemorySpy,
errorText: "getUserKeyFromMemory error",
},
{
method: "rsaEncrypt",
spy: () => cryptoSvcRsaEncryptSpy,
errorText: "rsaEncrypt error",
},
{
method: "encryptService.encrypt",
spy: () => encryptServiceEncryptSpy,
errorText: "encryptService.encrypt error",
},
];
describe.each(methodsToTestForErrorsOrInvalidReturns)(
"trustDevice error handling and invalid return testing",
({ method, spy, errorText }) => {
// ensures that error propagation works correctly
it(`throws an error if ${method} fails`, async () => {
const methodSpy = spy();
methodSpy.mockRejectedValue(new Error(errorText));
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(errorText);
});
test.each([null, undefined])(
`throws an error if ${method} returns %s`,
async (invalidValue) => {
const methodSpy = spy();
methodSpy.mockResolvedValue(invalidValue);
await expect(deviceCryptoService.trustDevice()).rejects.toThrow();
}
);
}
);
});
});
});