From df9e6e21c9312494921c4d652110512603a0a43c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 22 Sep 2022 07:51:14 -0500 Subject: [PATCH] Ps 1291/apply to from json pattern to state (#3425) * Clean up dangling behaviorSubject * Handle null in utils * fix null check * Await promises, even in async functions * Add to/fromJSON methods to State and Accounts This is needed since all storage in manifest v3 is key-value-pair-based and session storage of most data is actually serialized into an encrypted string. * Simplify AccountKeys json parsing * Fix account key (de)serialization * Remove unused DecodedToken state * Correct filename typo * Simplify keys `toJSON` tests * Explain AccountKeys `toJSON` return type * Remove unnecessary `any`s * Remove unique ArrayBuffer serialization * Initialize items in MemoryStorageService * Revert "Fix account key (de)serialization" This reverts commit b1dffb5c2cb7c02feec079704af52f7d9b07359b, which was breaking serializations * Move fromJSON to owning object * Add DeepJsonify type * Use Records for storage * Add new Account Settings to serialized data * Fix failing serialization tests * Extract complex type conversion to helper methods * Remove unnecessary decorator * Return null from json deserializers * Remove unnecessary decorators * Remove obsolete test * Use type-fest `Jsonify` formatting rules for external library * Update jsonify comment Co-authored-by: @eliykat * Remove erroneous comment * Fix unintended deep-jsonify changes * Fix prettierignore * Fix formatting of deep-jsonify.ts Co-authored-by: Thomas Rittson Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .prettierignore | 3 + .../session-sync.decorator.spec.ts | 1 + .../session-syncer.spec.ts | 5 +- .../session-sync-observable/session-syncer.ts | 4 +- .../sync-item-metadata.ts | 18 +-- .../synced-item-metadata.spec.ts | 56 +++---- .../services/abstractions/state.service.ts | 4 +- .../localBackedSessionStorage.service.spec.ts | 7 + .../localBackedSessionStorage.service.ts | 26 +++- .../src/services/state.service.spec.ts | 10 +- apps/browser/src/services/state.service.ts | 6 +- libs/common/spec/misc/utils.spec.ts | 6 + .../spec/services/state.service.spec.ts | 82 ---------- .../services/stateMigration.service.spec.ts | 4 +- libs/common/src/abstractions/state.service.ts | 2 - .../src/abstractions/storage.service.ts | 8 +- libs/common/src/misc/utils.ts | 3 + .../models/data/server-config.data.spec.ts | 55 +++++++ .../src/models/data/server-config.data.ts | 35 +++-- .../src/models/domain/account-keys.spec.ts | 62 ++++++++ .../src/models/domain/account-profile.spec.ts | 9 ++ .../models/domain/account-settings.spec.ts | 24 +++ .../src/models/domain/account-tokens.spec.ts | 9 ++ libs/common/src/models/domain/account.spec.ts | 23 +++ libs/common/src/models/domain/account.ts | 141 ++++++++++++++++- .../src/models/domain/encryption-pair.spec.ts | 34 +++++ .../src/models/domain/environmentUrls.ts | 6 + libs/common/src/models/domain/state.spec.ts | 28 ++++ libs/common/src/models/domain/state.ts | 28 ++++ .../src/models/domain/storageOptions.ts | 4 + libs/common/src/services/cipher.service.ts | 2 +- libs/common/src/services/encrypt.service.ts | 2 +- .../src/services/memoryStorage.service.ts | 10 +- libs/common/src/services/state.service.ts | 143 +++++------------- .../src/services/stateMigration.service.ts | 12 +- libs/common/src/services/token.service.ts | 5 - libs/common/src/types/deep-jsonify.ts | 44 ++++++ 37 files changed, 635 insertions(+), 286 deletions(-) delete mode 100644 libs/common/spec/services/state.service.spec.ts create mode 100644 libs/common/src/models/data/server-config.data.spec.ts create mode 100644 libs/common/src/models/domain/account-keys.spec.ts create mode 100644 libs/common/src/models/domain/account-profile.spec.ts create mode 100644 libs/common/src/models/domain/account-settings.spec.ts create mode 100644 libs/common/src/models/domain/account-tokens.spec.ts create mode 100644 libs/common/src/models/domain/account.spec.ts create mode 100644 libs/common/src/models/domain/encryption-pair.spec.ts create mode 100644 libs/common/src/models/domain/state.spec.ts create mode 100644 libs/common/src/types/deep-jsonify.ts diff --git a/.prettierignore b/.prettierignore index 8790a1e73f..b1c9359fa1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,3 +27,6 @@ libs/.github # Github Workflows .github/workflows + +# Forked library files +libs/common/src/types/deep-jsonify.ts diff --git a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts index c82f270021..b177d118f8 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts @@ -19,6 +19,7 @@ describe("sessionSync decorator", () => { ctor: ctor, initializer: initializer, }), + testClass.testProperty.complete(), ]); }); }); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts index f2df60ad7a..5286cece1b 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts @@ -5,6 +5,7 @@ import { BrowserApi } from "../../browser/browserApi"; import { StateService } from "../../services/abstractions/state.service"; import { SessionSyncer } from "./session-syncer"; +import { SyncedItemMetadata } from "./sync-item-metadata"; describe("session syncer", () => { const propertyKey = "behaviorSubject"; @@ -140,12 +141,14 @@ describe("session syncer", () => { }); it("should update from message on emit from another instance", async () => { + const builder = jest.fn(); + jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); stateService.getFromSessionMemory.mockResolvedValue("test"); await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" }); expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1); - expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey); + expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder); expect(nextSpy).toHaveBeenCalledTimes(1); expect(nextSpy).toHaveBeenCalledWith("test"); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts index 0c97b983f7..c757a44c7f 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts @@ -66,8 +66,8 @@ export class SessionSyncer { if (message.command != this.updateMessageCommand || message.id === this.id) { return; } - const keyValuePair = await this.stateService.getFromSessionMemory(this.metaData.sessionKey); - const value = SyncedItemMetadata.buildFromKeyValuePair(keyValuePair, this.metaData); + const builder = SyncedItemMetadata.builder(this.metaData); + const value = await this.stateService.getFromSessionMemory(this.metaData.sessionKey, builder); this.ignoreNextUpdate = true; this.behaviorSubject.next(value); } diff --git a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts index e225db6196..2b3f4715d4 100644 --- a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts +++ b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts @@ -5,19 +5,15 @@ export class SyncedItemMetadata { initializer?: (keyValuePair: any) => any; initializeAsArray?: boolean; - static buildFromKeyValuePair(keyValuePair: any, metadata: SyncedItemMetadata): any { - const builder = SyncedItemMetadata.getBuilder(metadata); - + static builder(metadata: SyncedItemMetadata): (o: any) => any { + const itemBuilder = + metadata.initializer != null + ? metadata.initializer + : (o: any) => Object.assign(new metadata.ctor(), o); if (metadata.initializeAsArray) { - return keyValuePair.map((o: any) => builder(o)); + return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o)); } else { - return builder(keyValuePair); + return (keyValuePair: any) => itemBuilder(keyValuePair); } } - - private static getBuilder(metadata: SyncedItemMetadata): (o: any) => any { - return metadata.initializer != null - ? metadata.initializer - : (o: any) => Object.assign(new metadata.ctor(), o); - } } diff --git a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts index da65be0490..5cd869a5b6 100644 --- a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts @@ -1,59 +1,39 @@ import { SyncedItemMetadata } from "./sync-item-metadata"; -describe("build from key value pair", () => { +describe("builder", () => { const propertyKey = "propertyKey"; const key = "key"; const initializer = (s: any) => "used initializer"; class TestClass {} const ctor = TestClass; - it("should call initializer if provided", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair( - {}, - { - propertyKey, - sessionKey: "key", - initializer: initializer, - } - ); - - expect(actual).toEqual("used initializer"); + it("should use initializer if provided", () => { + const metadata = { propertyKey, sessionKey: key, initializer }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBe("used initializer"); }); - it("should call ctor if provided", () => { - const expected = { provided: "value" }; - const actual = SyncedItemMetadata.buildFromKeyValuePair(expected, { - propertyKey, - sessionKey: key, - ctor: ctor, - }); - - expect(actual).toBeInstanceOf(ctor); - expect(actual).toEqual(expect.objectContaining(expected)); + it("should use ctor if initializer is not provided", () => { + const metadata = { propertyKey, sessionKey: key, ctor }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBeInstanceOf(TestClass); }); - it("should prefer using initializer if both are provided", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair( - {}, - { - propertyKey, - sessionKey: key, - initializer: initializer, - ctor: ctor, - } - ); - - expect(actual).toEqual("used initializer"); + it("should prefer initializer over ctor", () => { + const metadata = { propertyKey, sessionKey: key, ctor, initializer }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBe("used initializer"); }); it("should honor initialize as array", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair([1, 2], { + const metadata = { propertyKey, sessionKey: key, initializer: initializer, initializeAsArray: true, - }); - - expect(actual).toEqual(["used initializer", "used initializer"]); + }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder([{}])).toBeInstanceOf(Array); + expect(builder([{}])[0]).toBe("used initializer"); }); }); diff --git a/apps/browser/src/services/abstractions/state.service.ts b/apps/browser/src/services/abstractions/state.service.ts index ca2a5ced7f..2f0112ff64 100644 --- a/apps/browser/src/services/abstractions/state.service.ts +++ b/apps/browser/src/services/abstractions/state.service.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service"; import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; @@ -7,7 +9,7 @@ import { BrowserGroupingsComponentState } from "src/models/browserGroupingsCompo import { BrowserSendComponentState } from "src/models/browserSendComponentState"; export abstract class StateService extends BaseStateServiceAbstraction { - abstract getFromSessionMemory(key: string): Promise; + abstract getFromSessionMemory(key: string, deserializer?: (obj: Jsonify) => T): Promise; abstract setInSessionMemory(key: string, value: any): Promise; getBrowserGroupingComponentState: ( options?: StorageOptions diff --git a/apps/browser/src/services/localBackedSessionStorage.service.spec.ts b/apps/browser/src/services/localBackedSessionStorage.service.spec.ts index de88b6d8b2..f7101ddae2 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.spec.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.spec.ts @@ -96,6 +96,13 @@ describe("Browser Session Storage Service", () => { 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); + }); }); }); }); diff --git a/apps/browser/src/services/localBackedSessionStorage.service.ts b/apps/browser/src/services/localBackedSessionStorage.service.ts index cdc7d4cf15..dea2e75a5e 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.ts @@ -1,6 +1,12 @@ +import { Jsonify } from "type-fest"; + import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; -import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; +import { + AbstractCachedStorageService, + MemoryStorageServiceInterface, +} from "@bitwarden/common/abstractions/storage.service"; import { EncString } from "@bitwarden/common/models/domain/encString"; +import { MemoryStorageOptions } from "@bitwarden/common/models/domain/storageOptions"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; import { devFlag } from "../decorators/dev-flag.decorator"; @@ -15,7 +21,10 @@ const keys = { sessionKey: "session", }; -export class LocalBackedSessionStorageService extends AbstractCachedStorageService { +export class LocalBackedSessionStorageService + extends AbstractCachedStorageService + implements MemoryStorageServiceInterface +{ private cache = new Map(); private localStorage = new BrowserLocalStorageService(); private sessionStorage = new BrowserMemoryStorageService(); @@ -27,21 +36,26 @@ export class LocalBackedSessionStorageService extends AbstractCachedStorageServi super(); } - async get(key: string): Promise { + async get(key: string, options?: MemoryStorageOptions): Promise { if (this.cache.has(key)) { return this.cache.get(key) as T; } - return await this.getBypassCache(key); + return await this.getBypassCache(key, options); } - async getBypassCache(key: string): Promise { + async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { const session = await this.getLocalSession(await this.getSessionEncKey()); if (session == null || !Object.keys(session).includes(key)) { return null; } - this.cache.set(key, session[key]); + let value = session[key]; + if (options?.deserializer != null) { + value = options.deserializer(value as Jsonify); + } + + this.cache.set(key, value); return this.cache.get(key) as T; } diff --git a/apps/browser/src/services/state.service.spec.ts b/apps/browser/src/services/state.service.spec.ts index 60813c2293..f3b6c74a5e 100644 --- a/apps/browser/src/services/state.service.spec.ts +++ b/apps/browser/src/services/state.service.spec.ts @@ -1,8 +1,8 @@ -import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { - AbstractCachedStorageService, + MemoryStorageServiceInterface, AbstractStorageService, } from "@bitwarden/common/abstractions/storage.service"; import { SendType } from "@bitwarden/common/enums/sendType"; @@ -49,7 +49,7 @@ describe("Browser State Service", () => { }); describe("direct memory storage access", () => { - let memoryStorageService: AbstractCachedStorageService; + let memoryStorageService: LocalBackedSessionStorageService; beforeEach(() => { // We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass. @@ -79,12 +79,12 @@ describe("Browser State Service", () => { }); describe("state methods", () => { - let memoryStorageService: SubstituteOf; + let memoryStorageService: SubstituteOf; beforeEach(() => { memoryStorageService = Substitute.for(); const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state))); - memoryStorageService.get("state").mimicks(stateGetter); + memoryStorageService.get("state", Arg.any()).mimicks(stateGetter); sut = new StateService( diskStorageService, diff --git a/apps/browser/src/services/state.service.ts b/apps/browser/src/services/state.service.ts index 6685f495e0..78bc721031 100644 --- a/apps/browser/src/services/state.service.ts +++ b/apps/browser/src/services/state.service.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; import { GlobalState } from "@bitwarden/common/models/domain/globalState"; import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; @@ -17,9 +19,9 @@ export class StateService extends BaseStateService implements StateServiceAbstraction { - async getFromSessionMemory(key: string): Promise { + async getFromSessionMemory(key: string, deserializer?: (obj: Jsonify) => T): Promise { return this.memoryStorageService instanceof AbstractCachedStorageService - ? await this.memoryStorageService.getBypassCache(key) + ? await this.memoryStorageService.getBypassCache(key, { deserializer: deserializer }) : await this.memoryStorageService.get(key); } diff --git a/libs/common/spec/misc/utils.spec.ts b/libs/common/spec/misc/utils.spec.ts index d4dbac958a..3978f9cfed 100644 --- a/libs/common/spec/misc/utils.spec.ts +++ b/libs/common/spec/misc/utils.spec.ts @@ -70,4 +70,10 @@ describe("Utils Service", () => { expect(Utils.newGuid()).toMatch(validGuid); }); }); + + describe("fromByteStringToArray", () => { + it("should handle null", () => { + expect(Utils.fromByteStringToArray(null)).toEqual(null); + }); + }); }); diff --git a/libs/common/spec/services/state.service.spec.ts b/libs/common/spec/services/state.service.spec.ts deleted file mode 100644 index a6f76ab4fc..0000000000 --- a/libs/common/spec/services/state.service.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; - -import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; -import { StateFactory } from "@bitwarden/common/factories/stateFactory"; -import { Account } from "@bitwarden/common/models/domain/account"; -import { GlobalState } from "@bitwarden/common/models/domain/globalState"; -import { State } from "@bitwarden/common/models/domain/state"; -import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; -import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; -import { StateService } from "@bitwarden/common/services/state.service"; -import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; - -describe("Browser State Service backed by chrome.storage api", () => { - let secureStorageService: SubstituteOf; - let diskStorageService: SubstituteOf; - let memoryStorageService: SubstituteOf; - let logService: SubstituteOf; - let stateMigrationService: SubstituteOf; - let stateFactory: SubstituteOf>; - let useAccountCache: boolean; - - let state: State; - const userId = "userId"; - - let sut: StateService; - - beforeEach(() => { - secureStorageService = Substitute.for(); - diskStorageService = Substitute.for(); - memoryStorageService = Substitute.for(); - logService = Substitute.for(); - stateMigrationService = Substitute.for(); - stateFactory = Substitute.for(); - useAccountCache = true; - - state = new State(new GlobalState()); - const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state))); - memoryStorageService.get("state").mimicks(stateGetter); - memoryStorageService - .save("state", Arg.any(), Arg.any()) - .mimicks((key: string, obj: any, options: StorageOptions) => { - return new Promise(() => { - state = obj; - }); - }); - - sut = new StateService( - diskStorageService, - secureStorageService, - memoryStorageService, - logService, - stateMigrationService, - stateFactory, - useAccountCache - ); - }); - - describe("account state getters", () => { - beforeEach(() => { - state.accounts[userId] = createAccount(userId); - state.activeUserId = userId; - }); - - describe("getCryptoMasterKey", () => { - it("should return the stored SymmetricCryptoKey", async () => { - const key = new SymmetricCryptoKey(new Uint8Array(32).buffer); - state.accounts[userId].keys.cryptoMasterKey = key; - - const actual = await sut.getCryptoMasterKey(); - expect(actual).toBeInstanceOf(SymmetricCryptoKey); - expect(actual).toMatchObject(key); - }); - }); - }); - - function createAccount(userId: string): Account { - return new Account({ - profile: { userId: userId }, - }); - } -}); diff --git a/libs/common/spec/services/stateMigration.service.spec.ts b/libs/common/spec/services/stateMigration.service.spec.ts index e306e64e29..b0188abacc 100644 --- a/libs/common/spec/services/stateMigration.service.spec.ts +++ b/libs/common/spec/services/stateMigration.service.spec.ts @@ -116,8 +116,8 @@ describe("State Migration Service", () => { key: "orgThreeEncKey", }, }, - }, - }, + } as any, + } as any, }); const migratedAccount = await (stateMigrationService as any).migrateAccountFrom4To5( diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index 238dfe199d..94e24da419 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -78,8 +78,6 @@ export abstract class StateService { getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise; - getDecodedToken: (options?: StorageOptions) => Promise; - setDecodedToken: (value: any, options?: StorageOptions) => Promise; getDecryptedCiphers: (options?: StorageOptions) => Promise; setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise; getDecryptedCollections: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/abstractions/storage.service.ts b/libs/common/src/abstractions/storage.service.ts index 5cff9fdac6..31506f4302 100644 --- a/libs/common/src/abstractions/storage.service.ts +++ b/libs/common/src/abstractions/storage.service.ts @@ -1,4 +1,4 @@ -import { StorageOptions } from "../models/domain/storageOptions"; +import { MemoryStorageOptions, StorageOptions } from "../models/domain/storageOptions"; export abstract class AbstractStorageService { abstract get(key: string, options?: StorageOptions): Promise; @@ -8,5 +8,9 @@ export abstract class AbstractStorageService { } export abstract class AbstractCachedStorageService extends AbstractStorageService { - abstract getBypassCache(key: string, options?: StorageOptions): Promise; + abstract getBypassCache(key: string, options?: MemoryStorageOptions): Promise; +} + +export interface MemoryStorageServiceInterface { + get(key: string, options?: MemoryStorageOptions): Promise; } diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index f66124bc47..454def3655 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -99,6 +99,9 @@ export class Utils { } static fromByteStringToArray(str: string): Uint8Array { + if (str == null) { + return null; + } const arr = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt(i); diff --git a/libs/common/src/models/data/server-config.data.spec.ts b/libs/common/src/models/data/server-config.data.spec.ts new file mode 100644 index 0000000000..1c4e890ab8 --- /dev/null +++ b/libs/common/src/models/data/server-config.data.spec.ts @@ -0,0 +1,55 @@ +import { + EnvironmentServerConfigData, + ServerConfigData, + ThirdPartyServerConfigData, +} from "./server-config.data"; + +describe("ServerConfigData", () => { + describe("fromJSON", () => { + it("should create a ServerConfigData from a JSON object", () => { + const serverConfigData = ServerConfigData.fromJSON({ + version: "1.0.0", + gitHash: "1234567890", + server: { + name: "test", + url: "https://test.com", + }, + environment: { + vault: "https://vault.com", + api: "https://api.com", + identity: "https://identity.com", + notifications: "https://notifications.com", + sso: "https://sso.com", + }, + utcDate: "2020-01-01T00:00:00.000Z", + }); + + expect(serverConfigData.version).toEqual("1.0.0"); + expect(serverConfigData.gitHash).toEqual("1234567890"); + expect(serverConfigData.server.name).toEqual("test"); + expect(serverConfigData.server.url).toEqual("https://test.com"); + expect(serverConfigData.environment.vault).toEqual("https://vault.com"); + expect(serverConfigData.environment.api).toEqual("https://api.com"); + expect(serverConfigData.environment.identity).toEqual("https://identity.com"); + expect(serverConfigData.environment.notifications).toEqual("https://notifications.com"); + expect(serverConfigData.environment.sso).toEqual("https://sso.com"); + expect(serverConfigData.utcDate).toEqual("2020-01-01T00:00:00.000Z"); + }); + + it("should be an instance of ServerConfigData", () => { + const serverConfigData = ServerConfigData.fromJSON({} as any); + + expect(serverConfigData).toBeInstanceOf(ServerConfigData); + }); + + it("should deserialize sub objects", () => { + const serverConfigData = ServerConfigData.fromJSON({ + server: {}, + environment: {}, + } as any); + + expect(serverConfigData.server).toBeInstanceOf(ThirdPartyServerConfigData); + expect(serverConfigData.environment).toBeInstanceOf(EnvironmentServerConfigData); + }); + }); +}); diff --git a/libs/common/src/models/data/server-config.data.ts b/libs/common/src/models/data/server-config.data.ts index 62744ecb62..30043a0b02 100644 --- a/libs/common/src/models/data/server-config.data.ts +++ b/libs/common/src/models/data/server-config.data.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { ServerConfigResponse, ThirdPartyServerConfigResponse, @@ -11,27 +13,38 @@ export class ServerConfigData { environment?: EnvironmentServerConfigData; utcDate: string; - constructor(serverConfigReponse: ServerConfigResponse) { - this.version = serverConfigReponse?.version; - this.gitHash = serverConfigReponse?.gitHash; - this.server = serverConfigReponse?.server - ? new ThirdPartyServerConfigData(serverConfigReponse.server) + constructor(serverConfigResponse: Partial) { + this.version = serverConfigResponse?.version; + this.gitHash = serverConfigResponse?.gitHash; + this.server = serverConfigResponse?.server + ? new ThirdPartyServerConfigData(serverConfigResponse.server) : null; this.utcDate = new Date().toISOString(); - this.environment = serverConfigReponse?.environment - ? new EnvironmentServerConfigData(serverConfigReponse.environment) + this.environment = serverConfigResponse?.environment + ? new EnvironmentServerConfigData(serverConfigResponse.environment) : null; } + + static fromJSON(obj: Jsonify): ServerConfigData { + return Object.assign(new ServerConfigData({}), obj, { + server: obj?.server ? ThirdPartyServerConfigData.fromJSON(obj.server) : null, + environment: obj?.environment ? EnvironmentServerConfigData.fromJSON(obj.environment) : null, + }); + } } export class ThirdPartyServerConfigData { name: string; url: string; - constructor(response: ThirdPartyServerConfigResponse) { + constructor(response: Partial) { this.name = response.name; this.url = response.url; } + + static fromJSON(obj: Jsonify): ThirdPartyServerConfigData { + return Object.assign(new ThirdPartyServerConfigData({}), obj); + } } export class EnvironmentServerConfigData { @@ -41,11 +54,15 @@ export class EnvironmentServerConfigData { notifications: string; sso: string; - constructor(response: EnvironmentServerConfigResponse) { + constructor(response: Partial) { this.vault = response.vault; this.api = response.api; this.identity = response.identity; this.notifications = response.notifications; this.sso = response.sso; } + + static fromJSON(obj: Jsonify): EnvironmentServerConfigData { + return Object.assign(new EnvironmentServerConfigData({}), obj); + } } diff --git a/libs/common/src/models/domain/account-keys.spec.ts b/libs/common/src/models/domain/account-keys.spec.ts new file mode 100644 index 0000000000..bf8348e15a --- /dev/null +++ b/libs/common/src/models/domain/account-keys.spec.ts @@ -0,0 +1,62 @@ +import { Utils } from "@bitwarden/common/misc/utils"; + +import { makeStaticByteArray } from "../../../spec/utils"; + +import { AccountKeys, EncryptionPair } from "./account"; +import { SymmetricCryptoKey } from "./symmetricCryptoKey"; + +describe("AccountKeys", () => { + describe("toJSON", () => { + it("should serialize itself", () => { + const keys = new AccountKeys(); + const buffer = makeStaticByteArray(64).buffer; + keys.publicKey = buffer; + + const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString"); + keys.toJSON(); + expect(bufferSpy).toHaveBeenCalledWith(buffer); + }); + + it("should serialize public key as a string", () => { + const keys = new AccountKeys(); + keys.publicKey = Utils.fromByteStringToArray("hello").buffer; + const json = JSON.stringify(keys); + expect(json).toContain('"publicKey":"hello"'); + }); + }); + + describe("fromJSON", () => { + it("should deserialize public key to a buffer", () => { + const keys = AccountKeys.fromJSON({ + publicKey: "hello", + }); + expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello").buffer); + }); + + it("should deserialize cryptoMasterKey", () => { + const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); + AccountKeys.fromJSON({} as any); + expect(spy).toHaveBeenCalled(); + }); + + it("should deserialize organizationKeys", () => { + const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); + AccountKeys.fromJSON({ organizationKeys: [{ orgId: "keyJSON" }] } as any); + expect(spy).toHaveBeenCalled(); + }); + + it("should deserialize providerKeys", () => { + const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); + AccountKeys.fromJSON({ providerKeys: [{ providerId: "keyJSON" }] } as any); + expect(spy).toHaveBeenCalled(); + }); + + it("should deserialize privateKey", () => { + const spy = jest.spyOn(EncryptionPair, "fromJSON"); + AccountKeys.fromJSON({ + privateKey: { encrypted: "encrypted", decrypted: "decrypted" }, + } as any); + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/models/domain/account-profile.spec.ts b/libs/common/src/models/domain/account-profile.spec.ts new file mode 100644 index 0000000000..7c6deda34e --- /dev/null +++ b/libs/common/src/models/domain/account-profile.spec.ts @@ -0,0 +1,9 @@ +import { AccountProfile } from "./account"; + +describe("AccountProfile", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountProfile.fromJSON({})).toBeInstanceOf(AccountProfile); + }); + }); +}); diff --git a/libs/common/src/models/domain/account-settings.spec.ts b/libs/common/src/models/domain/account-settings.spec.ts new file mode 100644 index 0000000000..544d591b32 --- /dev/null +++ b/libs/common/src/models/domain/account-settings.spec.ts @@ -0,0 +1,24 @@ +import { AccountSettings, EncryptionPair } from "./account"; +import { EncString } from "./encString"; + +describe("AccountSettings", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings); + }); + + it("should deserialize pinProtected", () => { + const accountSettings = new AccountSettings(); + accountSettings.pinProtected = EncryptionPair.fromJSON({ + encrypted: "encrypted", + decrypted: "3.data", + }); + const jsonObj = JSON.parse(JSON.stringify(accountSettings)); + const actual = AccountSettings.fromJSON(jsonObj); + + expect(actual.pinProtected).toBeInstanceOf(EncryptionPair); + expect(actual.pinProtected.encrypted).toEqual("encrypted"); + expect(actual.pinProtected.decrypted.encryptedString).toEqual("3.data"); + }); + }); +}); diff --git a/libs/common/src/models/domain/account-tokens.spec.ts b/libs/common/src/models/domain/account-tokens.spec.ts new file mode 100644 index 0000000000..733b3908e9 --- /dev/null +++ b/libs/common/src/models/domain/account-tokens.spec.ts @@ -0,0 +1,9 @@ +import { AccountTokens } from "./account"; + +describe("AccountTokens", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); + }); + }); +}); diff --git a/libs/common/src/models/domain/account.spec.ts b/libs/common/src/models/domain/account.spec.ts new file mode 100644 index 0000000000..0c76c16cc2 --- /dev/null +++ b/libs/common/src/models/domain/account.spec.ts @@ -0,0 +1,23 @@ +import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; + +describe("Account", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(Account.fromJSON({})).toBeInstanceOf(Account); + }); + + it("should call all the sub-fromJSONs", () => { + const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); + const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); + const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); + const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); + + Account.fromJSON({}); + + expect(keysSpy).toHaveBeenCalled(); + expect(profileSpy).toHaveBeenCalled(); + expect(settingsSpy).toHaveBeenCalled(); + expect(tokensSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/models/domain/account.ts b/libs/common/src/models/domain/account.ts index b7bbcb431c..5822af35c0 100644 --- a/libs/common/src/models/domain/account.ts +++ b/libs/common/src/models/domain/account.ts @@ -1,3 +1,8 @@ +import { Except, Jsonify } from "type-fest"; + +import { Utils } from "@bitwarden/common/misc/utils"; +import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; + import { AuthenticationStatus } from "../../enums/authenticationStatus"; import { KdfType } from "../../enums/kdfType"; import { UriMatchType } from "../../enums/uriMatchType"; @@ -24,7 +29,39 @@ import { SymmetricCryptoKey } from "./symmetricCryptoKey"; export class EncryptionPair { encrypted?: TEncrypted; decrypted?: TDecrypted; - decryptedSerialized?: string; + + toJSON() { + return { + encrypted: this.encrypted, + decrypted: + this.decrypted instanceof ArrayBuffer + ? Utils.fromBufferToByteString(this.decrypted) + : this.decrypted, + }; + } + + static fromJSON( + obj: Jsonify, Jsonify>>, + decryptedFromJson?: (decObj: Jsonify | string) => TDecrypted, + encryptedFromJson?: (encObj: Jsonify) => TEncrypted + ) { + if (obj == null) { + return null; + } + + const pair = new EncryptionPair(); + if (obj?.encrypted != null) { + pair.encrypted = encryptedFromJson + ? encryptedFromJson(obj.encrypted) + : (obj.encrypted as TEncrypted); + } + if (obj?.decrypted != null) { + pair.decrypted = decryptedFromJson + ? decryptedFromJson(obj.decrypted) + : (obj.decrypted as TDecrypted); + } + return pair; + } } export class DataEncryptionPair { @@ -73,19 +110,66 @@ export class AccountKeys { >(); organizationKeys?: EncryptionPair< { [orgId: string]: EncryptedOrganizationKeyData }, - Map + Record > = new EncryptionPair< { [orgId: string]: EncryptedOrganizationKeyData }, - Map + Record >(); - providerKeys?: EncryptionPair> = new EncryptionPair< + providerKeys?: EncryptionPair> = new EncryptionPair< any, - Map + Record >(); privateKey?: EncryptionPair = new EncryptionPair(); publicKey?: ArrayBuffer; - publicKeySerialized?: string; apiKeyClientSecret?: string; + + toJSON() { + return Object.assign(this as Except, { + publicKey: Utils.fromBufferToByteString(this.publicKey), + }); + } + + static fromJSON(obj: DeepJsonify): AccountKeys { + if (obj == null) { + return null; + } + + return Object.assign( + new AccountKeys(), + { cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey) }, + { + cryptoSymmetricKey: EncryptionPair.fromJSON( + obj?.cryptoSymmetricKey, + SymmetricCryptoKey.fromJSON + ), + }, + { organizationKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.organizationKeys) }, + { providerKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.providerKeys) }, + { + privateKey: EncryptionPair.fromJSON( + obj?.privateKey, + (decObj: string) => Utils.fromByteStringToArray(decObj).buffer + ), + }, + { + publicKey: Utils.fromByteStringToArray(obj?.publicKey)?.buffer, + } + ); + } + + static initRecordEncryptionPairsFromJSON(obj: any) { + return EncryptionPair.fromJSON(obj, (decObj: any) => { + if (obj == null) { + return null; + } + + const record: Record = {}; + for (const id in decObj) { + record[id] = SymmetricCryptoKey.fromJSON(decObj[id]); + } + return record; + }); + } } export class AccountProfile { @@ -106,6 +190,14 @@ export class AccountProfile { keyHash?: string; kdfIterations?: number; kdfType?: KdfType; + + static fromJSON(obj: Jsonify): AccountProfile { + if (obj == null) { + return null; + } + + return Object.assign(new AccountProfile(), obj); + } } export class AccountSettings { @@ -142,6 +234,21 @@ export class AccountSettings { vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; serverConfig?: ServerConfigData; + + static fromJSON(obj: Jsonify): AccountSettings { + if (obj == null) { + return null; + } + + return Object.assign(new AccountSettings(), obj, { + environmentUrls: EnvironmentUrls.fromJSON(obj?.environmentUrls), + pinProtected: EncryptionPair.fromJSON( + obj?.pinProtected, + EncString.fromJSON + ), + serverConfig: ServerConfigData.fromJSON(obj?.serverConfig), + }); + } } export type AccountSettingsSettings = { @@ -150,9 +257,16 @@ export type AccountSettingsSettings = { export class AccountTokens { accessToken?: string; - decodedToken?: any; refreshToken?: string; securityStamp?: string; + + static fromJSON(obj: Jsonify): AccountTokens { + if (obj == null) { + return null; + } + + return Object.assign(new AccountTokens(), obj); + } } export class Account { @@ -186,4 +300,17 @@ export class Account { }, }); } + + static fromJSON(json: Jsonify): Account { + if (json == null) { + return null; + } + + return Object.assign(new Account({}), json, { + keys: AccountKeys.fromJSON(json?.keys), + profile: AccountProfile.fromJSON(json?.profile), + settings: AccountSettings.fromJSON(json?.settings), + tokens: AccountTokens.fromJSON(json?.tokens), + }); + } } diff --git a/libs/common/src/models/domain/encryption-pair.spec.ts b/libs/common/src/models/domain/encryption-pair.spec.ts new file mode 100644 index 0000000000..55fad76db0 --- /dev/null +++ b/libs/common/src/models/domain/encryption-pair.spec.ts @@ -0,0 +1,34 @@ +import { Utils } from "@bitwarden/common/misc/utils"; + +import { EncryptionPair } from "./account"; + +describe("EncryptionPair", () => { + describe("toJSON", () => { + it("should populate decryptedSerialized for buffer arrays", () => { + const pair = new EncryptionPair(); + pair.decrypted = Utils.fromByteStringToArray("hello").buffer; + const json = pair.toJSON(); + expect(json.decrypted).toEqual("hello"); + }); + + it("should serialize encrypted and decrypted", () => { + const pair = new EncryptionPair(); + pair.encrypted = "hello"; + pair.decrypted = "world"; + const json = pair.toJSON(); + expect(json.encrypted).toEqual("hello"); + expect(json.decrypted).toEqual("world"); + }); + }); + + describe("fromJSON", () => { + it("should deserialize encrypted and decrypted", () => { + const pair = EncryptionPair.fromJSON({ + encrypted: "hello", + decrypted: "world", + }); + expect(pair.encrypted).toEqual("hello"); + expect(pair.decrypted).toEqual("world"); + }); + }); +}); diff --git a/libs/common/src/models/domain/environmentUrls.ts b/libs/common/src/models/domain/environmentUrls.ts index d4fd173c83..c78768bdc2 100644 --- a/libs/common/src/models/domain/environmentUrls.ts +++ b/libs/common/src/models/domain/environmentUrls.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + export class EnvironmentUrls { base: string = null; api: string = null; @@ -7,4 +9,8 @@ export class EnvironmentUrls { events: string = null; webVault: string = null; keyConnector: string = null; + + static fromJSON(obj: Jsonify): EnvironmentUrls { + return Object.assign(new EnvironmentUrls(), obj); + } } diff --git a/libs/common/src/models/domain/state.spec.ts b/libs/common/src/models/domain/state.spec.ts new file mode 100644 index 0000000000..64e71d7cb2 --- /dev/null +++ b/libs/common/src/models/domain/state.spec.ts @@ -0,0 +1,28 @@ +import { Account } from "./account"; +import { State } from "./state"; + +describe("state", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(State.fromJSON({})).toBeInstanceOf(State); + }); + + it("should always assign an object to accounts", () => { + const state = State.fromJSON({}); + expect(state.accounts).not.toBeNull(); + expect(state.accounts).toEqual({}); + }); + + it("should build an account map", () => { + const accountsSpy = jest.spyOn(Account, "fromJSON"); + const state = State.fromJSON({ + accounts: { + userId: {}, + }, + }); + + expect(state.accounts["userId"]).toBeInstanceOf(Account); + expect(accountsSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/models/domain/state.ts b/libs/common/src/models/domain/state.ts index f5a2c046b5..5450325d25 100644 --- a/libs/common/src/models/domain/state.ts +++ b/libs/common/src/models/domain/state.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Account } from "./account"; import { GlobalState } from "./globalState"; @@ -14,4 +16,30 @@ export class State< constructor(globals: TGlobalState) { this.globals = globals; } + + // TODO, make Jsonify work. It currently doesn't because Globals doesn't implement Jsonify. + static fromJSON( + obj: any + ): State { + if (obj == null) { + return null; + } + + return Object.assign(new State(null), obj, { + accounts: State.buildAccountMapFromJSON(obj?.accounts), + }); + } + + private static buildAccountMapFromJSON( + jsonAccounts: Jsonify<{ [userId: string]: Jsonify }> + ) { + if (!jsonAccounts) { + return {}; + } + const accounts: { [userId: string]: Account } = {}; + for (const userId in jsonAccounts) { + accounts[userId] = Account.fromJSON(jsonAccounts[userId]); + } + return accounts; + } } diff --git a/libs/common/src/models/domain/storageOptions.ts b/libs/common/src/models/domain/storageOptions.ts index 2db7e0ccf5..7e21b19433 100644 --- a/libs/common/src/models/domain/storageOptions.ts +++ b/libs/common/src/models/domain/storageOptions.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { HtmlStorageLocation } from "../../enums/htmlStorageLocation"; import { StorageLocation } from "../../enums/storageLocation"; @@ -8,3 +10,5 @@ export type StorageOptions = { htmlStorageLocation?: HtmlStorageLocation; keySuffix?: string; }; + +export type MemoryStorageOptions = StorageOptions & { deserializer?: (obj: Jsonify) => T }; diff --git a/libs/common/src/services/cipher.service.ts b/libs/common/src/services/cipher.service.ts index 40440ea8bf..62d849d417 100644 --- a/libs/common/src/services/cipher.service.ts +++ b/libs/common/src/services/cipher.service.ts @@ -393,7 +393,7 @@ export class CipherService implements CipherServiceAbstraction { : firstValueFrom(this.settingsService.settings$).then( (settings: AccountSettingsSettings) => { let matches: any[] = []; - settings.equivalentDomains.forEach((eqDomain: any) => { + settings.equivalentDomains?.forEach((eqDomain: any) => { if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { matches = matches.concat(eqDomain); } diff --git a/libs/common/src/services/encrypt.service.ts b/libs/common/src/services/encrypt.service.ts index 0b3f9731ca..fef33a3f9c 100644 --- a/libs/common/src/services/encrypt.service.ts +++ b/libs/common/src/services/encrypt.service.ts @@ -98,7 +98,7 @@ export class EncryptService implements AbstractEncryptService { } } - return this.cryptoFunctionService.aesDecryptFast(fastParams); + return await this.cryptoFunctionService.aesDecryptFast(fastParams); } async decryptToBytes(encThing: IEncrypted, key: SymmetricCryptoKey): Promise { diff --git a/libs/common/src/services/memoryStorage.service.ts b/libs/common/src/services/memoryStorage.service.ts index d1616c6029..1634b67b05 100644 --- a/libs/common/src/services/memoryStorage.service.ts +++ b/libs/common/src/services/memoryStorage.service.ts @@ -1,6 +1,12 @@ -import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; +import { + AbstractStorageService, + MemoryStorageServiceInterface, +} from "@bitwarden/common/abstractions/storage.service"; -export class MemoryStorageService implements AbstractStorageService { +export class MemoryStorageService + extends AbstractStorageService + implements MemoryStorageServiceInterface +{ private store = new Map(); get(key: string): Promise { diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index d5aabb3490..7b6e59eb2c 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -3,14 +3,16 @@ import { BehaviorSubject, concatMap } from "rxjs"; import { LogService } from "../abstractions/log.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { StateMigrationService } from "../abstractions/stateMigration.service"; -import { AbstractStorageService } from "../abstractions/storage.service"; +import { + MemoryStorageServiceInterface, + AbstractStorageService, +} from "../abstractions/storage.service"; import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; import { KdfType } from "../enums/kdfType"; import { StorageLocation } from "../enums/storageLocation"; import { ThemeType } from "../enums/themeType"; import { UriMatchType } from "../enums/uriMatchType"; import { StateFactory } from "../factories/stateFactory"; -import { Utils } from "../misc/utils"; import { CipherData } from "../models/data/cipherData"; import { CollectionData } from "../models/data/collectionData"; import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData"; @@ -76,7 +78,7 @@ export class StateService< constructor( protected storageService: AbstractStorageService, protected secureStorageService: AbstractStorageService, - protected memoryStorageService: AbstractStorageService, + protected memoryStorageService: AbstractStorageService & MemoryStorageServiceInterface, protected logService: LogService, protected stateMigrationService: StateMigrationService, protected stateFactory: StateFactory, @@ -150,6 +152,9 @@ export class StateService< return; } await this.updateState(async (state) => { + if (state.accounts == null) { + state.accounts = {}; + } state.accounts[userId] = this.createAccount(); const diskAccount = await this.getAccountFromDisk({ userId: userId }); state.accounts[userId].profile = diskAccount.profile; @@ -494,11 +499,11 @@ export class StateService< ); } - @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getCryptoMasterKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.cryptoMasterKey; + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()) + ); + return account?.keys?.cryptoMasterKey; } async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { @@ -604,23 +609,6 @@ export class StateService< await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); } - async getDecodedToken(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.decodedToken; - } - - async setDecodedToken(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()) - ); - account.tokens.decodedToken = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()) - ); - } - @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON) async getDecryptedCiphers(options?: StorageOptions): Promise { return ( @@ -657,11 +645,11 @@ export class StateService< ); } - @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.cryptoSymmetricKey?.decrypted; + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()) + ); + return account?.keys?.cryptoSymmetricKey?.decrypted; } async setDecryptedCryptoSymmetricKey( @@ -678,14 +666,13 @@ export class StateService< ); } - @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedOrganizationKeys( options?: StorageOptions ): Promise> { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - return account?.keys?.organizationKeys?.decrypted; + return this.recordToMap(account?.keys?.organizationKeys?.decrypted); } async setDecryptedOrganizationKeys( @@ -695,7 +682,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - account.keys.organizationKeys.decrypted = value; + account.keys.organizationKeys.decrypted = this.mapToRecord(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -725,7 +712,6 @@ export class StateService< ); } - @withPrototype(EncString) async getDecryptedPinProtected(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -762,14 +748,9 @@ export class StateService< } async getDecryptedPrivateKey(options?: StorageOptions): Promise { - const privateKey = ( + return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.privateKey; - let result = privateKey?.decrypted; - if (result == null && privateKey?.decryptedSerialized != null) { - result = Utils.fromByteStringToArray(privateKey.decryptedSerialized); - } - return result; + )?.keys?.privateKey.decrypted; } async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise { @@ -777,21 +758,19 @@ export class StateService< this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.keys.privateKey.decrypted = value; - account.keys.privateKey.decryptedSerialized = - value == null ? null : Utils.fromBufferToByteString(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } - @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedProviderKeys( options?: StorageOptions ): Promise> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.providerKeys?.decrypted; + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()) + ); + return this.recordToMap(account?.keys?.providerKeys?.decrypted); } async setDecryptedProviderKeys( @@ -801,7 +780,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - account.keys.providerKeys.decrypted = value; + account.keys.providerKeys.decrypted = this.mapToRecord(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -1538,7 +1517,6 @@ export class StateService< ); } - @withPrototype(EnvironmentUrls) async getEnvironmentUrls(options?: StorageOptions): Promise { if ((await this.state())?.activeUserId == null) { return await this.getGlobalEnvironmentUrls(options); @@ -2021,11 +1999,7 @@ export class StateService< const keys = ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) )?.keys; - let result = keys?.publicKey; - if (result == null && keys?.publicKeySerialized != null) { - result = Utils.fromByteStringToArray(keys.publicKeySerialized); - } - return result; + return keys?.publicKey; } async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise { @@ -2033,7 +2007,6 @@ export class StateService< this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.keys.publicKey = value; - account.keys.publicKeySerialized = value == null ? null : Utils.fromBufferToByteString(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -2741,8 +2714,11 @@ export class StateService< : await this.secureStorageService.save(`${options.userId}${key}`, value, options); } - protected state(): Promise> { - return this.memoryStorageService.get>(keys.state); + protected async state(): Promise> { + const state = await this.memoryStorageService.get>(keys.state, { + deserializer: (s) => State.fromJSON(s), + }); + return state; } private async setState(state: State): Promise { @@ -2761,6 +2737,14 @@ export class StateService< await this.setState(updatedState); }); } + + private mapToRecord(map: Map): Record { + return map == null ? null : Object.fromEntries(map); + } + + private recordToMap(record: Record): Map { + return record == null ? null : new Map(Object.entries(record)); + } } export function withPrototype( @@ -2893,52 +2877,3 @@ function withPrototypeForObjectValues( }; }; } - -function withPrototypeForMap( - valuesConstructor: new (...args: any[]) => T, - valuesConverter: (input: any) => T = (i) => i -): ( - target: any, - propertyKey: string | symbol, - descriptor: PropertyDescriptor -) => { value: (...args: any[]) => Promise> } { - return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - - return { - value: function (...args: any[]) { - const originalResult: Promise = originalMethod.apply(this, args); - - if (!(originalResult instanceof Promise)) { - throw new Error( - `Error applying prototype to stored value -- result is not a promise for method ${String( - propertyKey - )}` - ); - } - - return originalResult.then((result) => { - if (result == null) { - return null; - } else if (result instanceof Map) { - return result; - } else { - for (const key in Object.keys(result)) { - result[key] = - result[key] == null || - result[key].constructor.name === valuesConstructor.prototype.constructor.name - ? valuesConverter(result[key]) - : valuesConverter( - Object.create( - valuesConstructor.prototype, - Object.getOwnPropertyDescriptors(result[key]) - ) - ); - } - return new Map(Object.entries(result)); - } - }); - }, - }; - }; -} diff --git a/libs/common/src/services/stateMigration.service.ts b/libs/common/src/services/stateMigration.service.ts index 359b4acec4..e936860657 100644 --- a/libs/common/src/services/stateMigration.service.ts +++ b/libs/common/src/services/stateMigration.service.ts @@ -12,7 +12,13 @@ import { OrganizationData } from "../models/data/organizationData"; import { PolicyData } from "../models/data/policyData"; import { ProviderData } from "../models/data/providerData"; import { SendData } from "../models/data/sendData"; -import { Account, AccountSettings, AccountSettingsSettings } from "../models/domain/account"; +import { + Account, + AccountSettings, + AccountSettingsSettings, + EncryptionPair, +} from "../models/domain/account"; +import { EncString } from "../models/domain/encString"; import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; import { GlobalState } from "../models/domain/globalState"; @@ -314,10 +320,10 @@ export class StateMigrationService< passwordGenerationOptions: (await this.get(v1Keys.passwordGenerationOptions)) ?? defaultAccount.settings.passwordGenerationOptions, - pinProtected: { + pinProtected: Object.assign(new EncryptionPair(), { decrypted: null, encrypted: await this.get(v1Keys.pinProtected), - }, + }), protectedPin: await this.get(v1Keys.protectedPin), settings: userId == null diff --git a/libs/common/src/services/token.service.ts b/libs/common/src/services/token.service.ts index 0922fa8751..5b6ffe6d88 100644 --- a/libs/common/src/services/token.service.ts +++ b/libs/common/src/services/token.service.ts @@ -93,11 +93,6 @@ export class TokenService implements TokenServiceAbstraction { // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js async decodeToken(token?: string): Promise { - const storedToken = await this.stateService.getDecodedToken(); - if (token === null && storedToken != null) { - return storedToken; - } - token = token ?? (await this.stateService.getAccessToken()); if (token == null) { diff --git a/libs/common/src/types/deep-jsonify.ts b/libs/common/src/types/deep-jsonify.ts new file mode 100644 index 0000000000..a83b8e4549 --- /dev/null +++ b/libs/common/src/types/deep-jsonify.ts @@ -0,0 +1,44 @@ +import { + PositiveInfinity, + NegativeInfinity, + JsonPrimitive, + TypedArray, + JsonValue, +} from "type-fest"; +import { NotJsonable } from "type-fest/source/jsonify"; + +/** + * Extracted from type-fest and extended with Jsonification of objects returned from `toJSON` methods. + */ +export type DeepJsonify = + // Check if there are any non-JSONable types represented in the union. + // Note: The use of tuples in this first condition side-steps distributive conditional types + // (see https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532) + [Extract] extends [never] + ? T extends PositiveInfinity | NegativeInfinity ? null + : T extends JsonPrimitive + ? T // Primitive is acceptable + : T extends number ? number + : T extends string ? string + : T extends boolean ? boolean + : T extends Map | Set ? Record // {} + : T extends TypedArray ? Record + : T extends Array + ? Array> // It's an array: recursive call for its children + : T extends object + ? T extends { toJSON(): infer J } + ? (() => J) extends () => JsonValue // Is J assignable to JsonValue? + ? J // Then T is Jsonable and its Jsonable value is J + : {[P in keyof J as P extends symbol + ? never + : J[P] extends NotJsonable + ? never + : P]: DeepJsonify[P]>; + } // Not Jsonable because its toJSON() method does not return JsonValue + : {[P in keyof T as P extends symbol + ? never + : T[P] extends NotJsonable + ? never + : P]: DeepJsonify[P]>} // It's an object: recursive call for its children + : never // Otherwise any other non-object is removed + : never; // Otherwise non-JSONable type union was found not empty