mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +00:00
Add State Provider Framework (#6640)
* Add StateDefinition
Add a class for encapsulation information about state
this will often be for a domain but creations of this will
exist outside of a specific domain, hence just the name State.
* Add KeyDefinition
This adds a type that extends state definition into another sub-key
and forces creators to define the data that will be stored and how
to read the data that they expect to be stored.
* Add key-builders helper functions
Adds to function to help building keys for both keys scoped
to a specific user and for keys scoped to global storage.
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
* Add updates$ stream to existing storageServices
Original commit by Matt: 823d9546fe
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
* Add fromChromeEvent helper
Create a helper that creats an Observable from a chrome event
and removes the listener when the subscription is completed.
* Implement `updates$` property for chrome storage
Use fromChromeEvent to create an observable from chrome
event and map that into our expected shape.
* Add GlobalState Abstractions
* Add UserState Abstractions
* Add Default Implementations of User/Global state
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
* Add Barrel File for state
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
* Fix ChromeStorageServices
* Rework fromChromeEvent
Rework fromChromeEvent so we have to lie to TS less and
remove unneeded generics. I did this by caring less about
the function and more about the parameters only.
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
* Fix UserStateProvider Test
* Add Inner Mock & Assert Calls
* Update Tests to use new keys
Use different key format
* Prefer returns over mutations in update
* Update Tests
* Address PR Feedback
* Be stricter with userId parameter
* Add Better Way To Determine if it was a remove
* Fix Web & Browser Storage Services
* Fix Desktop & CLI Storage Services
* Fix Test Storage Service
* Use createKey Helper
* Prefer implement to extending
* Determine storage location in providers
* Export default providers publicly
* Fix user state tests
* Name tests
* Fix CLI
* Prefer Implement In Chrome Storage
* Remove Secure Storage Option
Also throw an exception for subscribes to the secure storage observable.
* Update apps/browser/src/platform/browser/from-chrome-event.ts
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
* Enforce state module barrel file
* Fix Linting Error
* Allow state module import from other modules
* Globally Unregister fromChromeEvent Listeners
Changed fromChromeEvent to add its listeners through the BrowserApi, so that
they will be unregistered when safari closes.
* Test default global state
* Use Proper Casing in Parameter
* Address Feedback
* Update libs/common/src/platform/state/key-definition.ts
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
* Add `buildCacheKey` Method
* Fix lint errors
* Add Comment
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
* Use Generic in callback parameter
* Refactor Out DerivedStateDefinition
* Persist Listener Return Type
* Add Ticket Link
---------
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
@@ -193,6 +193,9 @@ export class BrowserApi {
|
||||
}
|
||||
|
||||
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
|
||||
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
||||
// and test that it doesn't break.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return chrome.windows.onCreated.addListener(callback);
|
||||
}
|
||||
|
||||
@@ -220,8 +223,10 @@ export class BrowserApi {
|
||||
|
||||
// Keep track of all the events registered in a Safari popup so we can remove
|
||||
// them when the popup gets unloaded, otherwise we cause a memory leak
|
||||
private static registeredMessageListeners: any[] = [];
|
||||
private static registeredStorageChangeListeners: any[] = [];
|
||||
private static trackedChromeEventListeners: [
|
||||
event: chrome.events.Event<(...args: unknown[]) => unknown>,
|
||||
callback: (...args: unknown[]) => unknown
|
||||
][] = [];
|
||||
|
||||
static messageListener(
|
||||
name: string,
|
||||
@@ -231,13 +236,7 @@ export class BrowserApi {
|
||||
sendResponse: any
|
||||
) => boolean | void
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chrome.runtime.onMessage.addListener(callback);
|
||||
|
||||
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
|
||||
BrowserApi.registeredMessageListeners.push(callback);
|
||||
BrowserApi.setupUnloadListeners();
|
||||
}
|
||||
BrowserApi.addListener(chrome.runtime.onMessage, callback);
|
||||
}
|
||||
|
||||
static messageListener$() {
|
||||
@@ -246,44 +245,67 @@ export class BrowserApi {
|
||||
subscriber.next(message);
|
||||
};
|
||||
|
||||
BrowserApi.messageListener("message", handler);
|
||||
BrowserApi.addListener(chrome.runtime.onMessage, handler);
|
||||
|
||||
return () => {
|
||||
chrome.runtime.onMessage.removeListener(handler);
|
||||
|
||||
if (BrowserApi.isSafariApi) {
|
||||
const index = BrowserApi.registeredMessageListeners.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
BrowserApi.registeredMessageListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
return () => BrowserApi.removeListener(chrome.runtime.onMessage, handler);
|
||||
});
|
||||
}
|
||||
|
||||
static storageChangeListener(
|
||||
callback: Parameters<typeof chrome.storage.onChanged.addListener>[0]
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chrome.storage.onChanged.addListener(callback);
|
||||
BrowserApi.addListener(chrome.storage.onChanged, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a callback to the given chrome event in a cross-browser platform manner.
|
||||
*
|
||||
* **Important:** All event listeners in the browser extension popup context must
|
||||
* use this instead of the native APIs to handle unsubscribing from Safari properly.
|
||||
*
|
||||
* @param event - The event in which to add the listener to.
|
||||
* @param callback - The callback you want registered onto the event.
|
||||
*/
|
||||
static addListener<T extends (...args: readonly unknown[]) => unknown>(
|
||||
event: chrome.events.Event<T>,
|
||||
callback: T
|
||||
) {
|
||||
event.addListener(callback);
|
||||
|
||||
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
|
||||
BrowserApi.registeredStorageChangeListeners.push(callback);
|
||||
BrowserApi.trackedChromeEventListeners.push([event, callback]);
|
||||
BrowserApi.setupUnloadListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a callback from the given chrome event in a cross-browser platform manner.
|
||||
* @param event - The event in which to remove the listener from.
|
||||
* @param callback - The callback you want removed from the event.
|
||||
*/
|
||||
static removeListener<T extends (...args: readonly unknown[]) => unknown>(
|
||||
event: chrome.events.Event<T>,
|
||||
callback: T
|
||||
) {
|
||||
event.removeListener(callback);
|
||||
|
||||
if (BrowserApi.isSafariApi && !BrowserApi.isBackgroundPage(window)) {
|
||||
const index = BrowserApi.trackedChromeEventListeners.findIndex(([_event, eventListener]) => {
|
||||
return eventListener == callback;
|
||||
});
|
||||
if (index !== -1) {
|
||||
BrowserApi.trackedChromeEventListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the event to destroy all the listeners when the popup gets unloaded in Safari, otherwise we get a memory leak
|
||||
private static setupUnloadListeners() {
|
||||
// The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well
|
||||
// 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one
|
||||
window.onpagehide = () => {
|
||||
for (const callback of BrowserApi.registeredMessageListeners) {
|
||||
chrome.runtime.onMessage.removeListener(callback);
|
||||
}
|
||||
|
||||
for (const callback of BrowserApi.registeredStorageChangeListeners) {
|
||||
chrome.storage.onChanged.removeListener(callback);
|
||||
for (const [event, callback] of BrowserApi.trackedChromeEventListeners) {
|
||||
event.removeListener(callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
103
apps/browser/src/platform/browser/from-chrome-event.spec.ts
Normal file
103
apps/browser/src/platform/browser/from-chrome-event.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { fromChromeEvent } from "./from-chrome-event";
|
||||
|
||||
describe("fromChromeEvent", () => {
|
||||
class FakeEvent implements chrome.events.Event<(arg1: string, arg2: number) => void> {
|
||||
listenerWasAdded: boolean;
|
||||
listenerWasRemoved: boolean;
|
||||
activeListeners: ((arg1: string, arg2: number) => void)[] = [];
|
||||
|
||||
addListener(callback: (arg1: string, arg2: number) => void): void {
|
||||
this.listenerWasAdded = true;
|
||||
this.activeListeners.push(callback);
|
||||
}
|
||||
getRules(callback: (rules: chrome.events.Rule[]) => void): void;
|
||||
getRules(ruleIdentifiers: string[], callback: (rules: chrome.events.Rule[]) => void): void;
|
||||
getRules(ruleIdentifiers: unknown, callback?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
hasListener(callback: (arg1: string, arg2: number) => void): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeRules(ruleIdentifiers?: string[], callback?: () => void): void;
|
||||
removeRules(callback?: () => void): void;
|
||||
removeRules(ruleIdentifiers?: unknown, callback?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
addRules(rules: chrome.events.Rule[], callback?: (rules: chrome.events.Rule[]) => void): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeListener(callback: (arg1: string, arg2: number) => void): void {
|
||||
const index = this.activeListeners.findIndex((c) => c == callback);
|
||||
if (index === -1) {
|
||||
throw new Error("No registered callback.");
|
||||
}
|
||||
|
||||
this.listenerWasRemoved = true;
|
||||
this.activeListeners.splice(index, 1);
|
||||
}
|
||||
hasListeners(): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
fireEvent(arg1: string, arg2: number) {
|
||||
this.activeListeners.forEach((listener) => {
|
||||
listener(arg1, arg2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let event: FakeEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
event = new FakeEvent();
|
||||
});
|
||||
|
||||
it("should never call addListener when never subscribed to", () => {
|
||||
fromChromeEvent(event);
|
||||
expect(event.listenerWasAdded).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should add a listener when subscribed to.", () => {
|
||||
const eventObservable = fromChromeEvent(event);
|
||||
eventObservable.subscribe();
|
||||
expect(event.listenerWasAdded).toBeTruthy();
|
||||
expect(event.activeListeners).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should call remove listener when the created subscription is unsubscribed", () => {
|
||||
const eventObservable = fromChromeEvent(event);
|
||||
const subscription = eventObservable.subscribe();
|
||||
subscription.unsubscribe();
|
||||
expect(event.listenerWasAdded).toBeTruthy();
|
||||
expect(event.listenerWasRemoved).toBeTruthy();
|
||||
expect(event.activeListeners).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fire each callback given to subscribe", () => {
|
||||
const eventObservable = fromChromeEvent(event);
|
||||
|
||||
let subscription1Called = false;
|
||||
let subscription2Called = false;
|
||||
|
||||
const subscription1 = eventObservable.subscribe(([arg1, arg2]) => {
|
||||
expect(arg1).toBe("Hi!");
|
||||
expect(arg2).toBe(2);
|
||||
subscription1Called = true;
|
||||
});
|
||||
|
||||
const subscription2 = eventObservable.subscribe(([arg1, arg2]) => {
|
||||
expect(arg1).toBe("Hi!");
|
||||
expect(arg2).toBe(2);
|
||||
subscription2Called = true;
|
||||
});
|
||||
|
||||
event.fireEvent("Hi!", 2);
|
||||
|
||||
subscription1.unsubscribe();
|
||||
subscription2.unsubscribe();
|
||||
|
||||
expect(event.activeListeners).toHaveLength(0);
|
||||
expect(subscription1Called).toBeTruthy();
|
||||
expect(subscription2Called).toBeTruthy();
|
||||
});
|
||||
});
|
||||
39
apps/browser/src/platform/browser/from-chrome-event.ts
Normal file
39
apps/browser/src/platform/browser/from-chrome-event.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { BrowserApi } from "./browser-api";
|
||||
|
||||
/**
|
||||
* Converts a Chrome event to an Observable stream.
|
||||
*
|
||||
* @typeParam T - The type of the event arguments.
|
||||
* @param event - The Chrome event to convert.
|
||||
* @returns An Observable stream of the event arguments.
|
||||
*
|
||||
* @remarks
|
||||
* This function creates an Observable stream that listens to a Chrome event and emits its arguments
|
||||
* whenever the event is triggered. If the event throws an error, the Observable will emit an error
|
||||
* notification with the error message.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const onMessage = fromChromeEvent(chrome.runtime.onMessage);
|
||||
* onMessage.subscribe((message) => console.log('Received message:', message));
|
||||
* ```
|
||||
*/
|
||||
export function fromChromeEvent<T extends unknown[]>(
|
||||
event: chrome.events.Event<(...args: T) => void>
|
||||
): Observable<T> {
|
||||
return new Observable<T>((subscriber) => {
|
||||
const handler = (...args: T) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
subscriber.error(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
|
||||
subscriber.next(args);
|
||||
};
|
||||
|
||||
BrowserApi.addListener(event, handler);
|
||||
return () => BrowserApi.removeListener(event, handler);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,39 @@
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Observable, mergeMap } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
StorageUpdate,
|
||||
StorageUpdateType,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
|
||||
import { fromChromeEvent } from "../../browser/from-chrome-event";
|
||||
|
||||
export default abstract class AbstractChromeStorageService implements AbstractStorageService {
|
||||
protected abstract chromeStorageApi: chrome.storage.StorageArea;
|
||||
constructor(protected chromeStorageApi: chrome.storage.StorageArea) {}
|
||||
|
||||
get updates$(): Observable<StorageUpdate> {
|
||||
return fromChromeEvent(this.chromeStorageApi.onChanged).pipe(
|
||||
mergeMap(([changes]) => {
|
||||
return Object.entries(changes).map(([key, change]) => {
|
||||
// The `newValue` property isn't on the StorageChange object
|
||||
// when the change was from a remove. Similarly a check of the `oldValue`
|
||||
// could be used to tell if the operation was the first creation of this key
|
||||
// but we currently do not differentiate that.
|
||||
// Ref: https://developer.chrome.com/docs/extensions/reference/storage/#type-StorageChange
|
||||
// Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/StorageChange
|
||||
const updateType: StorageUpdateType = "newValue" in change ? "save" : "remove";
|
||||
|
||||
return {
|
||||
key: key,
|
||||
// For removes this property will not exist but then it will just be
|
||||
// undefined which is fine.
|
||||
value: change.newValue,
|
||||
updateType: updateType,
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -22,11 +54,7 @@ export default abstract class AbstractChromeStorageService implements AbstractSt
|
||||
async save(key: string, obj: any): Promise<void> {
|
||||
if (obj == null) {
|
||||
// Fix safari not liking null in set
|
||||
return new Promise<void>((resolve) => {
|
||||
this.chromeStorageApi.remove(key, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return this.remove(key);
|
||||
}
|
||||
|
||||
if (obj instanceof Set) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
|
||||
|
||||
export default class BrowserLocalStorageService extends AbstractChromeStorageService {
|
||||
protected chromeStorageApi = chrome.storage.local;
|
||||
constructor() {
|
||||
super(chrome.storage.local);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
|
||||
|
||||
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
|
||||
protected chromeStorageApi = chrome.storage.session;
|
||||
constructor() {
|
||||
super(chrome.storage.session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Subject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
@@ -22,6 +26,7 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
||||
private cache = new Map<string, unknown>();
|
||||
private localStorage = new BrowserLocalStorageService();
|
||||
private sessionStorage = new BrowserMemoryStorageService();
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
constructor(
|
||||
private encryptService: EncryptService,
|
||||
@@ -30,6 +35,10 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
||||
super();
|
||||
}
|
||||
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
|
||||
if (this.cache.has(key)) {
|
||||
return this.cache.get(key) as T;
|
||||
|
||||
@@ -219,6 +219,9 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
});
|
||||
|
||||
this.windowClosed$ = fromEventPattern(
|
||||
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
||||
// and test that it doesn't break. Tracking Ticket: https://bitwarden.atlassian.net/browse/PM-4735
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(handler: any) => chrome.windows.onRemoved.addListener(handler),
|
||||
(handler: any) => chrome.windows.onRemoved.removeListener(handler)
|
||||
);
|
||||
|
||||
@@ -5,10 +5,14 @@ import * as lowdb from "lowdb";
|
||||
import * as FileSync from "lowdb/adapters/FileSync";
|
||||
import * as lock from "proper-lockfile";
|
||||
import { OperationOptions } from "retry";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { sequentialize } from "@bitwarden/common/platform/misc/sequentialize";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -24,6 +28,7 @@ export class LowdbStorageService implements AbstractStorageService {
|
||||
private db: lowdb.LowdbSync<any>;
|
||||
private defaults: any;
|
||||
private ready = false;
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
constructor(
|
||||
protected logService: LogService,
|
||||
@@ -102,6 +107,10 @@ export class LowdbStorageService implements AbstractStorageService {
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
@@ -119,21 +128,23 @@ export class LowdbStorageService implements AbstractStorageService {
|
||||
return this.get(key).then((v) => v != null);
|
||||
}
|
||||
|
||||
async save(key: string, obj: any): Promise<any> {
|
||||
async save(key: string, obj: any): Promise<void> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.set(key, obj).write();
|
||||
this.updatesSubject.next({ key, value: obj, updateType: "save" });
|
||||
this.logService.debug(`Successfully wrote ${key} to db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<any> {
|
||||
async remove(key: string): Promise<void> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.unset(key).write();
|
||||
this.updatesSubject.next({ key, value: null, updateType: "remove" });
|
||||
this.logService.debug(`Successfully removed ${key} from db`);
|
||||
return;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { throwError } from "rxjs";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
@@ -12,6 +14,12 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
private cryptoService: () => CryptoService
|
||||
) {}
|
||||
|
||||
get updates$() {
|
||||
return throwError(
|
||||
() => new Error("Secure storage implementations cannot have their updates subscribed to.")
|
||||
);
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T> {
|
||||
const value = await this.storageService.get<string>(this.makeProtectedStorageKey(key));
|
||||
if (value == null) {
|
||||
@@ -25,7 +33,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
return (await this.get(key)) != null;
|
||||
}
|
||||
|
||||
async save(key: string, obj: any): Promise<any> {
|
||||
async save(key: string, obj: any): Promise<void> {
|
||||
if (obj == null) {
|
||||
return this.remove(key);
|
||||
}
|
||||
@@ -37,8 +45,9 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
await this.storageService.save(this.makeProtectedStorageKey(key), protectedObj);
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
return this.storageService.remove(this.makeProtectedStorageKey(key));
|
||||
async remove(key: string): Promise<void> {
|
||||
await this.storageService.remove(this.makeProtectedStorageKey(key));
|
||||
return;
|
||||
}
|
||||
|
||||
private async encrypt(plainValue: string): Promise<string> {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { throwError } from "rxjs";
|
||||
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
|
||||
export class ElectronRendererSecureStorageService implements AbstractStorageService {
|
||||
get updates$() {
|
||||
return throwError(
|
||||
() => new Error("Secure storage implementations cannot have their updates subscribed to.")
|
||||
);
|
||||
}
|
||||
|
||||
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||
const val = await ipc.platform.passwords.get(key, options?.keySuffix ?? "");
|
||||
return val != null ? (JSON.parse(val) as T) : null;
|
||||
@@ -12,11 +20,11 @@ export class ElectronRendererSecureStorageService implements AbstractStorageServ
|
||||
return !!val;
|
||||
}
|
||||
|
||||
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
|
||||
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
|
||||
await ipc.platform.passwords.set(key, options?.keySuffix ?? "", JSON.stringify(obj));
|
||||
}
|
||||
|
||||
async remove(key: string, options?: StorageOptions): Promise<any> {
|
||||
async remove(key: string, options?: StorageOptions): Promise<void> {
|
||||
await ipc.platform.passwords.delete(key, options?.keySuffix ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
|
||||
export class ElectronRendererStorageService implements AbstractStorageService {
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
return ipc.platform.storage.get(key);
|
||||
}
|
||||
@@ -9,11 +20,13 @@ export class ElectronRendererStorageService implements AbstractStorageService {
|
||||
return ipc.platform.storage.has(key);
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
return ipc.platform.storage.save(key, obj);
|
||||
async save<T>(key: string, obj: T): Promise<void> {
|
||||
await ipc.platform.storage.save(key, obj);
|
||||
this.updatesSubject.next({ key, value: obj, updateType: "save" });
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
return ipc.platform.storage.remove(key);
|
||||
async remove(key: string): Promise<void> {
|
||||
await ipc.platform.storage.remove(key);
|
||||
this.updatesSubject.next({ key, value: null, updateType: "remove" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
|
||||
// See: https://github.com/sindresorhus/electron-store/blob/main/index.d.ts
|
||||
interface ElectronStoreOptions {
|
||||
@@ -35,6 +39,7 @@ type Options = BaseOptions<"get"> | BaseOptions<"has"> | SaveOptions | BaseOptio
|
||||
|
||||
export class ElectronStorageService implements AbstractStorageService {
|
||||
private store: ElectronStore;
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
constructor(dir: string, defaults = {}) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
@@ -60,6 +65,10 @@ export class ElectronStorageService implements AbstractStorageService {
|
||||
});
|
||||
}
|
||||
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
const val = this.store.get(key) as T;
|
||||
return Promise.resolve(val != null ? val : null);
|
||||
@@ -75,11 +84,13 @@ export class ElectronStorageService implements AbstractStorageService {
|
||||
obj = Array.from(obj);
|
||||
}
|
||||
this.store.set(key, obj);
|
||||
this.updatesSubject.next({ key, value: obj, updateType: "save" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string): Promise<void> {
|
||||
this.store.delete(key);
|
||||
this.updatesSubject.next({ key, value: null, updateType: "remove" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { HtmlStorageLocation } from "@bitwarden/common/enums";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
|
||||
@Injectable()
|
||||
export class HtmlStorageService implements AbstractStorageService {
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
|
||||
get defaultOptions(): StorageOptions {
|
||||
return { htmlStorageLocation: HtmlStorageLocation.Session };
|
||||
}
|
||||
|
||||
get updates$() {
|
||||
return this.updatesSubject.asObservable();
|
||||
}
|
||||
|
||||
get<T>(key: string, options: StorageOptions = this.defaultOptions): Promise<T> {
|
||||
let json: string = null;
|
||||
switch (options.htmlStorageLocation) {
|
||||
@@ -52,6 +62,7 @@ export class HtmlStorageService implements AbstractStorageService {
|
||||
window.sessionStorage.setItem(key, json);
|
||||
break;
|
||||
}
|
||||
this.updatesSubject.next({ key, value: obj, updateType: "save" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -65,6 +76,7 @@ export class HtmlStorageService implements AbstractStorageService {
|
||||
window.sessionStorage.removeItem(key);
|
||||
break;
|
||||
}
|
||||
this.updatesSubject.next({ key, value: null, updateType: "remove" });
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user