mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-05 09:10:53 +01:00
399b8c2b34
* Use abstract methods and generics in StorageService * Prepend `Abstract` to abstract classes * Create session browser storage service * Use memory storage service for state memory * Inject memory storage service * Maintain filename extensions to help ide formatting * Preserve state if it's still in memory * Use jslib's memory storage service * linter * Create prototypes on stored objects * standardize package scripts * Add type safety to `withPrototype` decorators * webpack notify manifest version * Fix desktop * linter * Fix script * Improve prototye application * do not change prototype if it already matches desired * fix error with object values prototype application * Handle null state * Apply prototypes to browser-specific state * Add angular language server to recommended extensions * Improve browser state service tests * Start testing state Service * Fix abstract returns * Move test setup files to not be picked up by default glob matchers * Add key generation service * Add low-dependency encrypt service * Back crypto service with encrypt service. We'll want to work items that don't require state over to encrypt service * Add new storage service and tests * Properly init more stored values * Fix reload issues when state service is recovering state from session storage Co-authored-by: Thomas Avery <Thomas-Avery@users.noreply.github.com> Co-authored-by: Justin Baur <admin@justinbaur.com> * Simplify encrypt service * Do not log mac failures for local-backed session storage * `content` changed to `main` in #2245 * Fix CLI * Remove loggin * PR feedback * Merge remote-tracking branch 'origin/master' into add-memory-storage-to-state-service * Fix desktop * Fix decrypt method signature * Minify if not development * Key is required Co-authored-by: Thomas Avery <Thomas-Avery@users.noreply.github.com> Co-authored-by: Justin Baur <admin@justinbaur.com>
309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|