1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

[PM-30307] Session key retrieval redesign for the local backed session storage (#18493)

* session key retrieval redesign for the local backed session storage

* typo

* incorrect substring

* get cache edge cases incorrectly handling to null values after removal

* test coverage

* internal `SessionKeyResolveService`
This commit is contained in:
Maciej Zieniuk
2026-01-29 16:14:41 +01:00
committed by GitHub
parent 3dcee2ef5d
commit 96ce13760b
4 changed files with 351 additions and 123 deletions

View File

@@ -139,8 +139,6 @@ import { IpcService, IpcSessionRepository } from "@bitwarden/common/platform/ipc
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
import {
@@ -565,36 +563,18 @@ export default class MainBackground {
this.memoryStorageService = this.memoryStorageForStateProviders;
}
this.encryptService = new EncryptServiceImplementation(
this.cryptoFunctionService,
this.logService,
true,
);
if (BrowserApi.isManifestVersion(3)) {
// Creates a session key for mv3 storage of large memory items
const sessionKey = new Lazy(async () => {
// Key already in session storage
const sessionStorage = new BrowserMemoryStorageService();
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
if (existingKey) {
if (sessionStorage.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(existingKey);
}
return existingKey;
}
// New key
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await sessionStorage.save("session-key", derivedKey.toJSON());
return derivedKey;
});
this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService(
sessionKey,
new BrowserMemoryStorageService(),
this.storageService,
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
// so that MAC failures are not logged.
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.keyGenerationService,
this.encryptService,
this.platformUtilsService,
this.logService,
);
@@ -629,12 +609,6 @@ export default class MainBackground {
storageServiceProvider,
);
this.encryptService = new EncryptServiceImplementation(
this.cryptoFunctionService,
this.logService,
true,
);
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
storageServiceProvider,
stateEventRegistrarService,

View File

@@ -15,6 +15,22 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer
return await this.getWithRetries<T>(key, 0);
}
/**
* Retrieves all storage keys.
*
* Returns all keys stored in local storage when the browser supports the getKeys API (Chrome 130+).
* Returns an empty array on older browser versions where this feature is unavailable.
*
* @returns Array of storage keys, or empty array if the feature is not supported
*/
async getKeys(): Promise<string[]> {
// getKeys function is only available since Chrome 130
if ("getKeys" in this.chromeStorageApi) {
return this.chromeStorageApi.getKeys();
}
return [];
}
private async getWithRetries<T>(key: string, retryNum: number): Promise<T> {
// See: https://github.com/EFForg/privacybadger/pull/2980
const MAX_RETRIES = 5;

View File

@@ -1,20 +1,89 @@
import { mock, MockProxy } from "jest-mock-extended";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeStorageService, makeEncString } from "@bitwarden/common/spec";
import { FakeStorageService, makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec";
import { StorageService } from "@bitwarden/storage-core";
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
import BrowserLocalStorageService from "./browser-local-storage.service";
import {
LocalBackedSessionStorageService,
SessionKeyResolveService,
} from "./local-backed-session-storage.service";
describe("SessionKeyResolveService", () => {
let storageService: FakeStorageService;
let keyGenerationService: MockProxy<KeyGenerationService>;
let sut: SessionKeyResolveService;
const mockKey = makeSymmetricCryptoKey();
beforeEach(() => {
storageService = new FakeStorageService();
keyGenerationService = mock<KeyGenerationService>();
sut = new SessionKeyResolveService(storageService, keyGenerationService);
});
describe("get", () => {
it("returns null when no session key exists", async () => {
const result = await sut.get();
expect(result).toBeNull();
});
it("returns the session key from storage", async () => {
await storageService.save("session-key", mockKey);
const result = await sut.get();
expect(result).toEqual(mockKey);
});
it("deserializes the session key when storage requires deserialization", async () => {
const mockStorageService = mock<FakeStorageService>();
Object.defineProperty(mockStorageService, "valuesRequireDeserialization", {
get: () => true,
});
mockStorageService.get.mockResolvedValue(mockKey.toJSON());
const deserializableSut = new SessionKeyResolveService(
mockStorageService,
keyGenerationService,
);
const result = await deserializableSut.get();
expect(result).toBeInstanceOf(SymmetricCryptoKey);
expect(result?.toJSON()).toEqual(mockKey.toJSON());
});
});
describe("create", () => {
it("creates a new session key and saves it to storage", async () => {
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
salt: "salt",
material: new Uint8Array(16) as any,
derivedKey: mockKey,
});
const result = await sut.create();
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledWith(
128,
"ephemeral",
"bitwarden-ephemeral",
);
expect(result).toEqual(mockKey);
expect(await storageService.get("session-key")).toEqual(mockKey.toJSON());
});
});
});
describe("LocalBackedSessionStorage", () => {
const sessionKey = new SymmetricCryptoKey(
Utils.fromUtf8ToArray("00000000000000000000000000000000"),
);
let localStorage: FakeStorageService;
const sessionKey = makeSymmetricCryptoKey();
let memoryStorage: MockProxy<StorageService>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let localStorage: MockProxy<BrowserLocalStorageService>;
let encryptService: MockProxy<EncryptService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let logService: MockProxy<LogService>;
@@ -22,14 +91,23 @@ describe("LocalBackedSessionStorage", () => {
let sut: LocalBackedSessionStorageService;
beforeEach(() => {
localStorage = new FakeStorageService();
memoryStorage = mock<StorageService>();
keyGenerationService = mock<KeyGenerationService>();
localStorage = mock<BrowserLocalStorageService>();
encryptService = mock<EncryptService>();
platformUtilsService = mock<PlatformUtilsService>();
logService = mock<LogService>();
// Default: session key exists
memoryStorage.get.mockResolvedValue(sessionKey);
Object.defineProperty(memoryStorage, "valuesRequireDeserialization", {
get: () => true,
});
sut = new LocalBackedSessionStorageService(
new Lazy(async () => sessionKey),
memoryStorage,
localStorage,
keyGenerationService,
encryptService,
platformUtilsService,
logService,
@@ -37,57 +115,79 @@ describe("LocalBackedSessionStorage", () => {
});
describe("get", () => {
it("return the cached value when one is cached", async () => {
const encString = makeEncString("encrypted");
it("returns the cached value when one is cached", async () => {
sut["cache"]["test"] = "cached";
const result = await sut.get("test");
expect(result).toEqual("cached");
});
it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test");
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
expect(result).toEqual("decrypted"));
});
it("returns null when both cache and storage are null", async () => {
sut["cache"]["test"] = null;
localStorage.get.mockResolvedValue(null);
it("caches the decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted");
const result = await sut.get("test");
expect(result).toBeNull();
expect(localStorage.get).toHaveBeenCalledWith("session_test");
});
it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
localStorage.get.mockResolvedValue(encString.encryptedString);
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test");
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
expect(result).toEqual("decrypted"));
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, sessionKey);
expect(result).toEqual("decrypted");
expect(sut["cache"]["test"]).toEqual("decrypted");
});
it("caches the decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted");
it("returns the cached value when cache is populated during storage retrieval", async () => {
localStorage.get.mockImplementation(async () => {
sut["cache"]["test"] = "cached-during-read";
return encString.encryptedString;
});
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted-from-storage"));
const result = await sut.get("test");
expect(result).toEqual("cached-during-read");
});
it("returns the cached value when storage returns null but cache was filled", async () => {
localStorage.get.mockImplementation(async () => {
sut["cache"]["test"] = "cached-during-read";
return null;
});
const result = await sut.get("test");
expect(result).toEqual("cached-during-read");
});
it("creates new session key, clears old data, and returns null when session key is missing", async () => {
const newSessionKey = makeSymmetricCryptoKey();
const clearSpy = jest.spyOn(sut as any, "clear");
memoryStorage.get.mockResolvedValue(null);
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
salt: "salt",
material: new Uint8Array(16) as any,
derivedKey: newSessionKey,
});
localStorage.get.mockResolvedValue(null);
localStorage.getKeys.mockResolvedValue([]);
const result = await sut.get("test");
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalled();
expect(clearSpy).toHaveBeenCalled();
expect(result).toBeNull();
});
});
describe("has", () => {
it("returns false when the key is not in cache", async () => {
const result = await sut.has("test");
expect(result).toBe(false);
});
it("returns true when the key is in cache", async () => {
sut["cache"]["test"] = "cached";
const result = await sut.has("test");
@@ -95,21 +195,17 @@ describe("LocalBackedSessionStorage", () => {
});
it("returns true when the key is in local storage", async () => {
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
const encString = makeEncString("encrypted");
localStorage.get.mockResolvedValue(encString.encryptedString);
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.has("test");
expect(result).toBe(true);
});
it.each([null, undefined])("returns false when %s is cached", async (nullish) => {
sut["cache"]["test"] = nullish;
await expect(sut.has("test")).resolves.toBe(false);
});
it.each([null, undefined])(
"returns false when null is stored in local storage",
async (nullish) => {
localStorage.internalStore["session_test"] = nullish;
"returns false when the key does not exist in local storage (%s)",
async (value) => {
localStorage.get.mockResolvedValue(value);
await expect(sut.has("test")).resolves.toBe(false);
expect(encryptService.decryptString).not.toHaveBeenCalled();
},
@@ -118,6 +214,7 @@ describe("LocalBackedSessionStorage", () => {
describe("save", () => {
const encString = makeEncString("encrypted");
beforeEach(() => {
encryptService.encryptString.mockResolvedValue(encString);
});
@@ -137,29 +234,44 @@ describe("LocalBackedSessionStorage", () => {
});
it("removes the key when saving a null value", async () => {
const spy = jest.spyOn(sut, "remove");
const removeSpy = jest.spyOn(sut, "remove");
await sut.save("test", null);
expect(spy).toHaveBeenCalledWith("test");
expect(removeSpy).toHaveBeenCalledWith("test");
});
it("saves the value to cache", async () => {
it("uses the session key when encrypting", async () => {
await sut.save("test", "value");
expect(sut["cache"]["test"]).toEqual("value");
});
it("encrypts and saves the value to local storage", async () => {
await sut.save("test", "value");
expect(memoryStorage.get).toHaveBeenCalledWith("session-key");
expect(encryptService.encryptString).toHaveBeenCalledWith(
JSON.stringify("value"),
sessionKey,
);
expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString);
});
it("emits an update", async () => {
const spy = jest.spyOn(sut["updatesSubject"], "next");
const updateSpy = jest.spyOn(sut["updatesSubject"], "next");
await sut.save("test", "value");
expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "save" });
expect(updateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" });
});
it("creates a new session key when session key is missing before saving", async () => {
const newSessionKey = makeSymmetricCryptoKey();
memoryStorage.get.mockResolvedValue(null);
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
salt: "salt",
material: new Uint8Array(16) as any,
derivedKey: newSessionKey,
});
localStorage.getKeys.mockResolvedValue([]);
await sut.save("test", "value");
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalled();
expect(encryptService.encryptString).toHaveBeenCalledWith(
JSON.stringify("value"),
newSessionKey,
);
});
});
@@ -171,15 +283,50 @@ describe("LocalBackedSessionStorage", () => {
});
it("removes the key from local storage", async () => {
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
await sut.remove("test");
expect(localStorage.internalStore["session_test"]).toBeUndefined();
expect(localStorage.remove).toHaveBeenCalledWith("session_test");
});
it("emits an update", async () => {
const spy = jest.spyOn(sut["updatesSubject"], "next");
const updateSpy = jest.spyOn(sut["updatesSubject"], "next");
await sut.remove("test");
expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
expect(updateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
});
});
describe("sessionStorageKey", () => {
it("prefixes keys with session_ prefix", () => {
expect(sut["sessionStorageKey"]("test")).toBe("session_test");
});
});
describe("clear", () => {
it("only removes keys with session_ prefix", async () => {
const removeSpy = jest.spyOn(sut, "remove");
localStorage.getKeys.mockResolvedValue([
"session_data1",
"session_data2",
"regular_key",
"another_key",
"session_data3",
"my_session_key",
"mysession",
"sessiondata",
"user_session",
]);
await sut["clear"]();
expect(removeSpy).toHaveBeenCalledWith("data1");
expect(removeSpy).toHaveBeenCalledWith("data2");
expect(removeSpy).toHaveBeenCalledWith("data3");
expect(removeSpy).not.toHaveBeenCalledWith("regular_key");
expect(removeSpy).not.toHaveBeenCalledWith("another_key");
expect(removeSpy).not.toHaveBeenCalledWith("my_session_key");
expect(removeSpy).not.toHaveBeenCalledWith("mysession");
expect(removeSpy).not.toHaveBeenCalledWith("sessiondata");
expect(removeSpy).not.toHaveBeenCalledWith("user_session");
expect(removeSpy).toHaveBeenCalledTimes(3);
});
});
});

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { Subject } from "rxjs";
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -12,33 +13,94 @@ import {
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { compareValues } from "@bitwarden/common/platform/misc/compare-values";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StorageService } from "@bitwarden/storage-core";
import { BrowserApi } from "../browser/browser-api";
import { MemoryStoragePortMessage } from "../storage/port-messages";
import { portName } from "../storage/port-name";
import BrowserLocalStorageService from "./browser-local-storage.service";
const SESSION_KEY_PREFIX = "session_";
/**
* Manages an ephemeral session key for encrypting session storage items persisted in local storage.
*
* The session key is stored in session storage and automatically cleared when the browser session ends
* (e.g., browser restart, extension reload). When the session key is unavailable, any encrypted items
* in local storage cannot be decrypted and must be cleared to maintain data consistency.
*
* This provides session-scoped security for sensitive data while using persistent local storage as the backing store.
*
* @internal Internal implementation detail. Exported only for testing purposes.
* Do not use this class directly outside of tests. Use LocalBackedSessionStorageService instead.
*/
export class SessionKeyResolveService {
constructor(
private readonly storageService: StorageService,
private readonly keyGenerationService: KeyGenerationService,
) {}
/**
* Retrieves the session key from storage.
*
* @return session key or null when not in storage
*/
async get(): Promise<SymmetricCryptoKey | null> {
const key = await this.storageService.get<SymmetricCryptoKey>("session-key");
if (key) {
if (this.storageService.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(key);
}
return key;
}
return null;
}
/**
* Creates new session key and adds it to underlying storage.
*
* @return newly created session key
*/
async create(): Promise<SymmetricCryptoKey> {
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await this.storageService.save("session-key", derivedKey.toJSON());
return derivedKey;
}
}
export class LocalBackedSessionStorageService
extends AbstractStorageService
implements ObservableStorageService
{
readonly valuesRequireDeserialization = true;
private ports: Set<chrome.runtime.Port> = new Set([]);
private cache: Record<string, unknown> = {};
private updatesSubject = new Subject<StorageUpdate>();
readonly valuesRequireDeserialization = true;
updates$ = this.updatesSubject.asObservable();
private readonly sessionKeyResolveService: SessionKeyResolveService;
constructor(
private readonly sessionKey: Lazy<Promise<SymmetricCryptoKey>>,
private readonly localStorage: AbstractStorageService,
private readonly memoryStorage: StorageService,
private readonly localStorage: BrowserLocalStorageService,
private readonly keyGenerationService: KeyGenerationService,
private readonly encryptService: EncryptService,
private readonly platformUtilsService: PlatformUtilsService,
private readonly logService: LogService,
) {
super();
this.sessionKeyResolveService = new SessionKeyResolveService(
this.memoryStorage,
this.keyGenerationService,
);
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
if (port.name !== portName(chrome.storage.session)) {
return;
@@ -70,20 +132,20 @@ export class LocalBackedSessionStorageService
}
async get<T>(key: string, options?: StorageOptions): Promise<T> {
if (this.cache[key] !== undefined) {
if (this.cache[key] != null) {
return this.cache[key] as T;
}
const value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
const value = await this.getLocalSessionValue(await this.getSessionKey(), key);
if (this.cache[key] === undefined && value !== undefined) {
if (this.cache[key] == null && value != null) {
// Cache is still empty and we just got a value from local/session storage, cache it.
this.cache[key] = value;
return value as T;
} else if (this.cache[key] === undefined && value === undefined) {
} else if (this.cache[key] == null && value == null) {
// Cache is still empty and we got nothing from local/session storage, no need to modify cache.
return value as T;
} else if (this.cache[key] !== undefined && value !== undefined) {
} else if (this.cache[key] != null && value != null) {
// Conflict, somebody wrote to the cache while we were reading from storage
// but we also got a value from storage. We assume the cache is more up to date
// and use that value.
@@ -91,7 +153,7 @@ export class LocalBackedSessionStorageService
`Conflict while reading from local session storage, both cache and storage have values. Key: ${key}. Using cached value.`,
);
return this.cache[key] as T;
} else if (this.cache[key] !== undefined && value === undefined) {
} else if (this.cache[key] != null && value == null) {
// Cache was filled after the local/session storage read completed. We got null
// from the storage read, but we have a value from the cache, use that.
this.logService.warning(
@@ -136,6 +198,44 @@ export class LocalBackedSessionStorageService
this.updatesSubject.next({ key, updateType: "remove" });
}
protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
this.ports.forEach((port) => {
this.sendMessageTo(port, data);
});
}
private async getSessionKey(): Promise<SymmetricCryptoKey> {
const sessionKey = await this.sessionKeyResolveService.get();
if (sessionKey != null) {
return sessionKey;
}
// Session key is missing (browser restart/extension reload), so all stored session data
// cannot be decrypted. Clear all items before creating a new session key.
await this.clear();
return await this.sessionKeyResolveService.create();
}
/**
* Removes all stored session data.
*
* Called when the session key is unavailable (typically after browser restart or extension reload),
* making all encrypted session data unrecoverable. Prevents orphaned encrypted data from accumulating.
*/
private async clear() {
const keys = (await this.localStorage.getKeys()).filter((key) =>
key.startsWith(SESSION_KEY_PREFIX),
);
this.logService.debug(
`[LocalBackedSessionStorageService] Clearing local session storage. Found ${keys}`,
);
for (const key of keys) {
const keyWithoutPrefix = key.substring(SESSION_KEY_PREFIX.length);
await this.remove(keyWithoutPrefix);
}
}
private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise<unknown> {
const local = await this.localStorage.get<string>(this.sessionStorageKey(key));
if (local == null) {
@@ -159,10 +259,7 @@ export class LocalBackedSessionStorageService
}
const valueJson = JSON.stringify(value);
const encValue = await this.encryptService.encryptString(
valueJson,
await this.sessionKey.get(),
);
const encValue = await this.encryptService.encryptString(valueJson, await this.getSessionKey());
await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString);
}
@@ -197,12 +294,6 @@ export class LocalBackedSessionStorageService
});
}
protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
this.ports.forEach((port) => {
this.sendMessageTo(port, data);
});
}
private sendMessageTo(
port: chrome.runtime.Port,
data: Omit<MemoryStoragePortMessage, "originator">,
@@ -214,7 +305,7 @@ export class LocalBackedSessionStorageService
}
private sessionStorageKey(key: string) {
return `session_${key}`;
return `${SESSION_KEY_PREFIX}${key}`;
}
private compareValues<T>(value1: T, value2: T): boolean {