JSON stringify memory items (#7731)

* JSON stringify memory items

stringification is required so they can be reliably sent through messaging

* Simplify null handling
This commit is contained in:
Matt Gibson 2024-01-29 14:42:58 -05:00 committed by GitHub
parent 76183c839a
commit 1da6733e71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 150 additions and 17 deletions

View File

@ -51,6 +51,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
import { import {
AbstractMemoryStorageService, AbstractMemoryStorageService,
AbstractStorageService, AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service"; } from "@bitwarden/common/platform/abstractions/storage.service";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
@ -62,6 +63,7 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { import {
@ -188,6 +190,7 @@ export default class MainBackground {
storageService: AbstractStorageService; storageService: AbstractStorageService;
secureStorageService: AbstractStorageService; secureStorageService: AbstractStorageService;
memoryStorageService: AbstractMemoryStorageService; memoryStorageService: AbstractMemoryStorageService;
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
i18nService: I18nServiceAbstraction; i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction;
logService: LogServiceAbstraction; logService: LogServiceAbstraction;
@ -309,6 +312,13 @@ export default class MainBackground {
this.storageService = new BrowserLocalStorageService(); this.storageService = new BrowserLocalStorageService();
this.secureStorageService = new BrowserLocalStorageService(); this.secureStorageService = new BrowserLocalStorageService();
this.memoryStorageService = this.memoryStorageService =
BrowserApi.manifestVersion === 3
? new LocalBackedSessionStorageService(
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
new KeyGenerationService(this.cryptoFunctionService),
)
: new MemoryStorageService();
this.memoryStorageForStateProviders =
BrowserApi.manifestVersion === 3 BrowserApi.manifestVersion === 3
? new LocalBackedSessionStorageService( ? new LocalBackedSessionStorageService(
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
@ -316,7 +326,7 @@ export default class MainBackground {
) )
: new BackgroundMemoryStorageService(); : new BackgroundMemoryStorageService();
this.globalStateProvider = new DefaultGlobalStateProvider( this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageService as BackgroundMemoryStorageService, this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService, this.storageService as BrowserLocalStorageService,
); );
this.encryptService = flagEnabled("multithreadDecryption") this.encryptService = flagEnabled("multithreadDecryption")
@ -328,7 +338,7 @@ export default class MainBackground {
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
this.singleUserStateProvider = new DefaultSingleUserStateProvider( this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageService as BackgroundMemoryStorageService, this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService, this.storageService as BrowserLocalStorageService,
); );
this.accountService = new AccountServiceImplementation( this.accountService = new AccountServiceImplementation(
@ -338,11 +348,11 @@ export default class MainBackground {
); );
this.activeUserStateProvider = new DefaultActiveUserStateProvider( this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService, this.accountService,
this.memoryStorageService as BackgroundMemoryStorageService, this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService, this.storageService as BrowserLocalStorageService,
); );
this.derivedStateProvider = new BackgroundDerivedStateProvider( this.derivedStateProvider = new BackgroundDerivedStateProvider(
this.memoryStorageService as BackgroundMemoryStorageService, this.memoryStorageForStateProviders,
); );
this.stateProvider = new DefaultStateProvider( this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider, this.activeUserStateProvider,

View File

@ -1,4 +1,5 @@
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage specifically for browser backgrounds
import { MemoryStorageService } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { BrowserApi } from "../browser/browser-api"; import { BrowserApi } from "../browser/browser-api";
@ -27,7 +28,7 @@ export class BackgroundMemoryStorageService extends MemoryStorageService {
// Initialize the new memory storage service with existing data // Initialize the new memory storage service with existing data
this.sendMessageTo(port, { this.sendMessageTo(port, {
action: "initialization", action: "initialization",
data: Array.from(this.store.keys()), data: Array.from(Object.keys(this.store)),
}); });
}); });
this.updates$.subscribe((update) => { this.updates$.subscribe((update) => {

View File

@ -60,6 +60,7 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */ /* eslint-enable import/no-restricted-paths */
import { AuditService } from "@bitwarden/common/services/audit.service"; import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
@ -125,6 +126,7 @@ export class Main {
storageService: LowdbStorageService; storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService; secureStorageService: NodeEnvSecureStorageService;
memoryStorageService: MemoryStorageService; memoryStorageService: MemoryStorageService;
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
i18nService: I18nService; i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService; platformUtilsService: CliPlatformUtilsService;
cryptoService: CryptoService; cryptoService: CryptoService;
@ -227,14 +229,15 @@ export class Main {
); );
this.memoryStorageService = new MemoryStorageService(); this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
this.globalStateProvider = new DefaultGlobalStateProvider( this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageService, this.memoryStorageForStateProviders,
this.storageService, this.storageService,
); );
this.singleUserStateProvider = new DefaultSingleUserStateProvider( this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageService, this.memoryStorageForStateProviders,
this.storageService, this.storageService,
); );
@ -248,11 +251,13 @@ export class Main {
this.activeUserStateProvider = new DefaultActiveUserStateProvider( this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService, this.accountService,
this.memoryStorageService, this.memoryStorageForStateProviders,
this.storageService, this.storageService,
); );
this.derivedStateProvider = new DefaultDerivedStateProvider(this.memoryStorageService); this.derivedStateProvider = new DefaultDerivedStateProvider(
this.memoryStorageForStateProviders,
);
this.stateProvider = new DefaultStateProvider( this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider, this.activeUserStateProvider,

View File

@ -39,6 +39,8 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { StateProvider } from "@bitwarden/common/platform/state"; import { StateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@ -107,7 +109,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
{ provide: AbstractStorageService, useClass: ElectronRendererStorageService }, { provide: AbstractStorageService, useClass: ElectronRendererStorageService },
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService }, { provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
{ provide: MEMORY_STORAGE, useClass: MemoryStorageService }, { provide: MEMORY_STORAGE, useClass: MemoryStorageService },
{ provide: OBSERVABLE_MEMORY_STORAGE, useExisting: MEMORY_STORAGE }, { provide: OBSERVABLE_MEMORY_STORAGE, useClass: MemoryStorageServiceForStateProviders },
{ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService }, { provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService },
{ {
provide: SystemServiceAbstraction, provide: SystemServiceAbstraction,

View File

@ -14,6 +14,7 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/*/ eslint-enable import/no-restricted-paths */ /*/ eslint-enable import/no-restricted-paths */
import { MenuMain } from "./main/menu/menu.main"; import { MenuMain } from "./main/menu/menu.main";
@ -38,6 +39,7 @@ export class Main {
i18nService: I18nMainService; i18nService: I18nMainService;
storageService: ElectronStorageService; storageService: ElectronStorageService;
memoryStorageService: MemoryStorageService; memoryStorageService: MemoryStorageService;
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
messagingService: ElectronMainMessagingService; messagingService: ElectronMainMessagingService;
stateService: ElectronStateService; stateService: ElectronStateService;
environmentService: EnvironmentService; environmentService: EnvironmentService;
@ -95,8 +97,9 @@ export class Main {
storageDefaults["global.vaultTimeoutAction"] = "lock"; storageDefaults["global.vaultTimeoutAction"] = "lock";
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults); this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService(); this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
const globalStateProvider = new DefaultGlobalStateProvider( const globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageService, this.memoryStorageForStateProviders,
this.storageService, this.storageService,
); );
@ -109,12 +112,12 @@ export class Main {
const stateProvider = new DefaultStateProvider( const stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider( new DefaultActiveUserStateProvider(
accountService, accountService,
this.memoryStorageService, this.memoryStorageForStateProviders,
this.storageService, this.storageService,
), ),
new DefaultSingleUserStateProvider(this.memoryStorageService, this.storageService), new DefaultSingleUserStateProvider(this.memoryStorageForStateProviders, this.storageService),
globalStateProvider, globalStateProvider,
new DefaultDerivedStateProvider(this.memoryStorageService), new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
); );
this.environmentService = new EnvironmentService(stateProvider, accountService); this.environmentService = new EnvironmentService(stateProvider, accountService);

View File

@ -30,6 +30,8 @@ import {
GlobalStateProvider, GlobalStateProvider,
SingleUserStateProvider, SingleUserStateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { PolicyListService } from "../admin-console/core/policy-list.service"; import { PolicyListService } from "../admin-console/core/policy-list.service";
import { HtmlStorageService } from "../core/html-storage.service"; import { HtmlStorageService } from "../core/html-storage.service";
@ -87,7 +89,7 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
provide: MEMORY_STORAGE, provide: MEMORY_STORAGE,
useClass: MemoryStorageService, useClass: MemoryStorageService,
}, },
{ provide: OBSERVABLE_MEMORY_STORAGE, useExisting: MEMORY_STORAGE }, { provide: OBSERVABLE_MEMORY_STORAGE, useClass: MemoryStorageServiceForStateProviders },
{ {
provide: OBSERVABLE_DISK_STORAGE, provide: OBSERVABLE_DISK_STORAGE,
useFactory: () => new WindowStorageService(window.sessionStorage), useFactory: () => new WindowStorageService(window.sessionStorage),

View File

@ -3,11 +3,12 @@ import { Subject } from "rxjs";
import { import {
AbstractStorageService, AbstractStorageService,
ObservableStorageService,
StorageUpdate, StorageUpdate,
} from "../src/platform/abstractions/storage.service"; } from "../src/platform/abstractions/storage.service";
import { StorageOptions } from "../src/platform/models/domain/storage-options"; import { StorageOptions } from "../src/platform/models/domain/storage-options";
export class FakeStorageService implements AbstractStorageService { export class FakeStorageService implements AbstractStorageService, ObservableStorageService {
private store: Record<string, unknown>; private store: Record<string, unknown>;
private updatesSubject = new Subject<StorageUpdate>(); private updatesSubject = new Subject<StorageUpdate>();
private _valuesRequireDeserialization = false; private _valuesRequireDeserialization = false;

View File

@ -0,0 +1,53 @@
import { MemoryStorageService } from "./memory-storage.service";
describe("MemoryStorageService", () => {
let sut: MemoryStorageService;
const key = "key";
const value = { test: "value" };
beforeEach(() => {
sut = new MemoryStorageService();
});
afterEach(() => {
jest.resetAllMocks();
});
describe("get", () => {
it("should return null if the key does not exist", async () => {
const result = await sut.get(key);
expect(result).toBeNull();
});
it("should return the value if the key exists", async () => {
await sut.save(key, value);
const result = await sut.get(key);
expect(result).toEqual(value);
});
it("should json parse stored values", async () => {
sut["store"][key] = JSON.stringify({ test: "value" });
const result = await sut.get(key);
expect(result).toEqual({ test: "value" });
});
});
describe("save", () => {
it("should store the value as json string", async () => {
const value = { test: "value" };
await sut.save(key, value);
expect(sut["store"][key]).toEqual(JSON.stringify(value));
});
});
describe("remove", () => {
it("should remove a value from store", async () => {
await sut.save(key, value);
await sut.remove(key);
expect(Object.keys(sut["store"])).not.toContain(key);
});
});
});

View File

@ -0,0 +1,56 @@
import { Subject } from "rxjs";
import {
AbstractMemoryStorageService,
ObservableStorageService,
StorageUpdate,
} from "../../abstractions/storage.service";
export class MemoryStorageService
extends AbstractMemoryStorageService
implements ObservableStorageService
{
protected store: Record<string, string> = {};
private updatesSubject = new Subject<StorageUpdate>();
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
}
get<T>(key: string): Promise<T> {
const json = this.store[key];
if (json) {
const obj = JSON.parse(json as string);
return Promise.resolve(obj as T);
}
return Promise.resolve(null);
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save<T>(key: string, obj: T): Promise<void> {
if (obj == null) {
return this.remove(key);
}
// TODO: Remove once foreground/background contexts are separated in browser
// Needed to ensure ownership of all memory by the context running the storage service
this.store[key] = JSON.stringify(obj);
this.updatesSubject.next({ key, updateType: "save" });
return Promise.resolve();
}
remove(key: string): Promise<void> {
delete this.store[key];
this.updatesSubject.next({ key, updateType: "remove" });
return Promise.resolve();
}
getBypassCache<T>(key: string): Promise<T> {
return this.get<T>(key);
}
}