bitwarden-estensione-browser/apps/browser/src/services/localBackedSessionStorage.s...

317 lines
11 KiB
TypeScript

// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { EncryptService } from "@bitwarden/common/src/services/encrypt.service";
import BrowserLocalStorageService from "./browserLocalStorage.service";
import BrowserMemoryStorageService from "./browserMemoryStorage.service";
import { KeyGenerationService } from "./keyGeneration.service";
import { LocalBackedSessionStorageService } from "./localBackedSessionStorage.service";
describe("Browser Session Storage Service", () => {
let encryptService: SubstituteOf<EncryptService>;
let keyGenerationService: SubstituteOf<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").buffer
);
let getSessionKeySpy: jest.SpyInstance;
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
let sut: LocalBackedSessionStorageService;
beforeEach(() => {
encryptService = Substitute.for();
keyGenerationService = Substitute.for();
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();
});
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.makeEphemeralKey().resolves(key);
jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
});
it("should create a symmetric crypto key", async () => {
const result = await sut.getSessionEncKey();
expect(result).toStrictEqual(key);
keyGenerationService.received(1).makeEphemeralKey();
});
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(encSession, key).resolves(decryptedSession);
await sut.getLocalSession(key);
encryptService.received(1).decryptToUtf8(encSession, key);
});
it("should parse session", async () => {
encryptService.decryptToUtf8(encSession, key).resolves(decryptedSession);
const result = await sut.getLocalSession(key);
expect(result).toEqual(session);
});
it("should remove state if decryption fails", async () => {
encryptService.decryptToUtf8(Arg.any(), Arg.any()).resolves(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(Arg.any(), Arg.any()).mimicks(mockEnc);
jest.spyOn(localStorage, "save").mockResolvedValue();
await sut.setLocalSession(testSession, key);
encryptService.received(1).encrypt(testJSON, key);
});
it("should remove local session if null", async () => {
encryptService.encrypt(Arg.any(), Arg.any()).resolves(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(Arg.any(), Arg.any()).mimicks(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);
});
});
});