mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-27 04:03:00 +02:00
19a373d87e
* create key generation service * replace old key generation service and add references * use key generation service in key connector service * use key generation service in send service * user key generation service in access service * use key generation service in device trust service * fix tests * fix browser * add createKeyFromMaterial and tests * create ephemeral key * fix tests * rename method and add returns docs * ignore material in destructure * modify test * specify material as key material * pull out magic strings to properties * make salt optional and generate if not provided * fix test * fix parameters * update docs to include link to HKDF rfc
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
import { mock, MockProxy } from "jest-mock-extended";
|
|
|
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
|
|
import BrowserLocalStorageService from "./browser-local-storage.service";
|
|
import BrowserMemoryStorageService from "./browser-memory-storage.service";
|
|
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
|
|
|
|
describe("Browser Session Storage Service", () => {
|
|
let encryptService: MockProxy<EncryptService>;
|
|
let keyGenerationService: MockProxy<KeyGenerationService>;
|
|
|
|
let cache: Map<string, any>;
|
|
const testObj = { a: 1, b: 2 };
|
|
|
|
let localStorage: BrowserLocalStorageService;
|
|
let sessionStorage: BrowserMemoryStorageService;
|
|
|
|
const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000"));
|
|
let getSessionKeySpy: jest.SpyInstance;
|
|
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
|
|
|
|
let sut: LocalBackedSessionStorageService;
|
|
|
|
beforeEach(() => {
|
|
encryptService = mock<EncryptService>();
|
|
keyGenerationService = mock<KeyGenerationService>();
|
|
|
|
sut = new LocalBackedSessionStorageService(encryptService, keyGenerationService);
|
|
|
|
cache = sut["cache"];
|
|
localStorage = sut["localStorage"];
|
|
sessionStorage = sut["sessionStorage"];
|
|
getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey");
|
|
getSessionKeySpy.mockResolvedValue(key);
|
|
});
|
|
|
|
it("should exist", () => {
|
|
expect(sut).toBeInstanceOf(LocalBackedSessionStorageService);
|
|
});
|
|
|
|
describe("get", () => {
|
|
it("should return from cache", async () => {
|
|
cache.set("test", testObj);
|
|
const result = await sut.get("test");
|
|
expect(result).toStrictEqual(testObj);
|
|
});
|
|
|
|
describe("not in cache", () => {
|
|
const session = { test: testObj };
|
|
|
|
beforeEach(() => {
|
|
jest.spyOn(sut, "getSessionEncKey").mockResolvedValue(key);
|
|
});
|
|
|
|
describe("no session retrieved", () => {
|
|
let result: any;
|
|
let spy: jest.SpyInstance;
|
|
beforeEach(async () => {
|
|
spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
|
|
result = await sut.get("test");
|
|
});
|
|
|
|
it("should grab from session if not in cache", async () => {
|
|
expect(spy).toHaveBeenCalledWith(key);
|
|
});
|
|
|
|
it("should return null if session is null", async () => {
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("session retrieved from storage", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(sut, "getLocalSession").mockResolvedValue(session);
|
|
});
|
|
|
|
it("should return null if session does not have the key", async () => {
|
|
const result = await sut.get("DNE");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return the value retrieved from session", async () => {
|
|
const result = await sut.get("test");
|
|
expect(result).toEqual(session.test);
|
|
});
|
|
|
|
it("should set retrieved values in cache", async () => {
|
|
await sut.get("test");
|
|
expect(cache.has("test")).toBe(true);
|
|
expect(cache.get("test")).toEqual(session.test);
|
|
});
|
|
|
|
it("should use a deserializer if provided", async () => {
|
|
const deserializer = jest.fn().mockReturnValue(testObj);
|
|
const result = await sut.get("test", { deserializer: deserializer });
|
|
expect(deserializer).toHaveBeenCalledWith(session.test);
|
|
expect(result).toEqual(testObj);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("has", () => {
|
|
it("should be false if `get` returns null", async () => {
|
|
const spy = jest.spyOn(sut, "get");
|
|
spy.mockResolvedValue(null);
|
|
expect(await sut.has("test")).toBe(false);
|
|
expect(spy).toHaveBeenCalledWith("test");
|
|
});
|
|
|
|
it("should be true if `get` returns non-null", async () => {
|
|
const spy = jest.spyOn(sut, "get");
|
|
spy.mockResolvedValue({});
|
|
expect(await sut.has("test")).toBe(true);
|
|
expect(spy).toHaveBeenCalledWith("test");
|
|
});
|
|
});
|
|
|
|
describe("remove", () => {
|
|
it("should save null", async () => {
|
|
const spy = jest.spyOn(sut, "save");
|
|
spy.mockResolvedValue(null);
|
|
await sut.remove("test");
|
|
expect(spy).toHaveBeenCalledWith("test", null);
|
|
});
|
|
});
|
|
|
|
describe("save", () => {
|
|
describe("caching", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(localStorage, "get").mockResolvedValue(null);
|
|
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
|
|
jest.spyOn(localStorage, "save").mockResolvedValue();
|
|
jest.spyOn(sessionStorage, "save").mockResolvedValue();
|
|
|
|
encryptService.encrypt.mockResolvedValue(mockEnc("{}"));
|
|
});
|
|
|
|
it("should remove key from cache if value is null", async () => {
|
|
cache.set("test", {});
|
|
const deleteSpy = jest.spyOn(cache, "delete");
|
|
expect(cache.has("test")).toBe(true);
|
|
await sut.save("test", null);
|
|
expect(cache.has("test")).toBe(false);
|
|
expect(deleteSpy).toHaveBeenCalledWith("test");
|
|
});
|
|
|
|
it("should set cache if value is non-null", async () => {
|
|
expect(cache.has("test")).toBe(false);
|
|
const setSpy = jest.spyOn(cache, "set");
|
|
await sut.save("test", testObj);
|
|
expect(cache.get("test")).toBe(testObj);
|
|
expect(setSpy).toHaveBeenCalledWith("test", testObj);
|
|
});
|
|
});
|
|
|
|
describe("local storing", () => {
|
|
let setSpy: jest.SpyInstance;
|
|
|
|
beforeEach(() => {
|
|
setSpy = jest.spyOn(sut, "setLocalSession").mockResolvedValue();
|
|
});
|
|
|
|
it("should store a new session", async () => {
|
|
jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
|
|
await sut.save("test", testObj);
|
|
|
|
expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key);
|
|
});
|
|
|
|
it("should update an existing session", async () => {
|
|
const existingObj = { test: testObj };
|
|
jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj);
|
|
await sut.save("test2", testObj);
|
|
|
|
expect(setSpy).toHaveBeenCalledWith({ test2: testObj, ...existingObj }, key);
|
|
});
|
|
|
|
it("should overwrite an existing item in session", async () => {
|
|
const existingObj = { test: {} };
|
|
jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj);
|
|
await sut.save("test", testObj);
|
|
|
|
expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getSessionKey", () => {
|
|
beforeEach(() => {
|
|
getSessionKeySpy.mockRestore();
|
|
});
|
|
|
|
it("should return the stored symmetric crypto key", async () => {
|
|
jest.spyOn(sessionStorage, "get").mockResolvedValue({ ...key });
|
|
const result = await sut.getSessionEncKey();
|
|
|
|
expect(result).toStrictEqual(key);
|
|
});
|
|
|
|
describe("new key creation", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
|
|
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
|
salt: "salt",
|
|
material: null,
|
|
derivedKey: key,
|
|
});
|
|
jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
|
});
|
|
|
|
it("should create a symmetric crypto key", async () => {
|
|
const result = await sut.getSessionEncKey();
|
|
|
|
expect(result).toStrictEqual(key);
|
|
expect(keyGenerationService.createKeyWithPurpose).toBeCalledTimes(1);
|
|
});
|
|
|
|
it("should store a symmetric crypto key if it makes one", async () => {
|
|
const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
|
await sut.getSessionEncKey();
|
|
|
|
expect(spy).toBeCalledWith(key);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getLocalSession", () => {
|
|
it("should return null if session is null", async () => {
|
|
const spy = jest.spyOn(localStorage, "get").mockResolvedValue(null);
|
|
const result = await sut.getLocalSession(key);
|
|
|
|
expect(result).toBeNull();
|
|
expect(spy).toBeCalledWith("session");
|
|
});
|
|
|
|
describe("non-null sessions", () => {
|
|
const session = { test: "test" };
|
|
const encSession = new EncString(JSON.stringify(session));
|
|
const decryptedSession = JSON.stringify(session);
|
|
|
|
beforeEach(() => {
|
|
jest.spyOn(localStorage, "get").mockResolvedValue(encSession.encryptedString);
|
|
});
|
|
|
|
it("should decrypt returned sessions", async () => {
|
|
encryptService.decryptToUtf8
|
|
.calledWith(expect.anything(), key)
|
|
.mockResolvedValue(decryptedSession);
|
|
await sut.getLocalSession(key);
|
|
expect(encryptService.decryptToUtf8).toHaveBeenNthCalledWith(1, encSession, key);
|
|
});
|
|
|
|
it("should parse session", async () => {
|
|
encryptService.decryptToUtf8
|
|
.calledWith(expect.anything(), key)
|
|
.mockResolvedValue(decryptedSession);
|
|
const result = await sut.getLocalSession(key);
|
|
expect(result).toEqual(session);
|
|
});
|
|
|
|
it("should remove state if decryption fails", async () => {
|
|
encryptService.decryptToUtf8.mockResolvedValue(null);
|
|
const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
|
const removeLocalSessionSpy = jest.spyOn(localStorage, "remove").mockResolvedValue();
|
|
|
|
const result = await sut.getLocalSession(key);
|
|
|
|
expect(result).toBeNull();
|
|
expect(setSessionEncKeySpy).toHaveBeenCalledWith(null);
|
|
expect(removeLocalSessionSpy).toHaveBeenCalledWith("session");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("setLocalSession", () => {
|
|
const testSession = { test: "a" };
|
|
const testJSON = JSON.stringify(testSession);
|
|
|
|
it("should encrypt a stringified session", async () => {
|
|
encryptService.encrypt.mockImplementation(mockEnc);
|
|
jest.spyOn(localStorage, "save").mockResolvedValue();
|
|
await sut.setLocalSession(testSession, key);
|
|
|
|
expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key);
|
|
});
|
|
|
|
it("should remove local session if null", async () => {
|
|
encryptService.encrypt.mockResolvedValue(null);
|
|
const spy = jest.spyOn(localStorage, "remove").mockResolvedValue();
|
|
await sut.setLocalSession(null, key);
|
|
|
|
expect(spy).toHaveBeenCalledWith("session");
|
|
});
|
|
|
|
it("should save encrypted string", async () => {
|
|
encryptService.encrypt.mockImplementation(mockEnc);
|
|
const spy = jest.spyOn(localStorage, "save").mockResolvedValue();
|
|
await sut.setLocalSession(testSession, key);
|
|
|
|
expect(spy).toHaveBeenCalledWith("session", (await mockEnc(testJSON)).encryptedString);
|
|
});
|
|
});
|
|
|
|
describe("setSessionKey", () => {
|
|
it("should remove if null", async () => {
|
|
const spy = jest.spyOn(sessionStorage, "remove").mockResolvedValue();
|
|
await sut.setSessionEncKey(null);
|
|
expect(spy).toHaveBeenCalledWith("localEncryptionKey");
|
|
});
|
|
|
|
it("should save key when not null", async () => {
|
|
const spy = jest.spyOn(sessionStorage, "save").mockResolvedValue();
|
|
await sut.setSessionEncKey(key);
|
|
expect(spy).toHaveBeenCalledWith("localEncryptionKey", key);
|
|
});
|
|
});
|
|
});
|