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 b1dffb5c2c, 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 <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Matt Gibson 2022-09-22 07:51:14 -05:00 committed by GitHub
parent 30f38dc916
commit df9e6e21c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 635 additions and 286 deletions

View File

@ -27,3 +27,6 @@ libs/.github
# Github Workflows
.github/workflows
# Forked library files
libs/common/src/types/deep-jsonify.ts

View File

@ -19,6 +19,7 @@ describe("sessionSync decorator", () => {
ctor: ctor,
initializer: initializer,
}),
testClass.testProperty.complete(),
]);
});
});

View File

@ -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");

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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");
});
});

View File

@ -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<Account> {
abstract getFromSessionMemory<T>(key: string): Promise<T>;
abstract getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T>;
abstract setInSessionMemory(key: string, value: any): Promise<void>;
getBrowserGroupingComponentState: (
options?: StorageOptions

View File

@ -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);
});
});
});
});

View File

@ -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<string, unknown>();
private localStorage = new BrowserLocalStorageService();
private sessionStorage = new BrowserMemoryStorageService();
@ -27,21 +36,26 @@ export class LocalBackedSessionStorageService extends AbstractCachedStorageServi
super();
}
async get<T>(key: string): Promise<T> {
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
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<T>(key: string): Promise<T> {
async getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
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<T>);
}
this.cache.set(key, value);
return this.cache.get(key) as T;
}

View File

@ -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<AbstractStorageService>;
let memoryStorageService: SubstituteOf<AbstractStorageService & MemoryStorageServiceInterface>;
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,

View File

@ -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<GlobalState, Account>
implements StateServiceAbstraction
{
async getFromSessionMemory<T>(key: string): Promise<T> {
async getFromSessionMemory<T>(key: string, deserializer?: (obj: Jsonify<T>) => T): Promise<T> {
return this.memoryStorageService instanceof AbstractCachedStorageService
? await this.memoryStorageService.getBypassCache<T>(key)
? await this.memoryStorageService.getBypassCache<T>(key, { deserializer: deserializer })
: await this.memoryStorageService.get<T>(key);
}

View File

@ -70,4 +70,10 @@ describe("Utils Service", () => {
expect(Utils.newGuid()).toMatch(validGuid);
});
});
describe("fromByteStringToArray", () => {
it("should handle null", () => {
expect(Utils.fromByteStringToArray(null)).toEqual(null);
});
});
});

View File

@ -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<AbstractStorageService>;
let diskStorageService: SubstituteOf<AbstractStorageService>;
let memoryStorageService: SubstituteOf<AbstractStorageService>;
let logService: SubstituteOf<LogService>;
let stateMigrationService: SubstituteOf<StateMigrationService>;
let stateFactory: SubstituteOf<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean;
let state: State<GlobalState, Account>;
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 },
});
}
});

View File

@ -116,8 +116,8 @@ describe("State Migration Service", () => {
key: "orgThreeEncKey",
},
},
},
},
} as any,
} as any,
});
const migratedAccount = await (stateMigrationService as any).migrateAccountFrom4To5(

View File

@ -78,8 +78,6 @@ export abstract class StateService<T extends Account = Account> {
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
getDecodedToken: (options?: StorageOptions) => Promise<any>;
setDecodedToken: (value: any, options?: StorageOptions) => Promise<void>;
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;

View File

@ -1,4 +1,4 @@
import { StorageOptions } from "../models/domain/storageOptions";
import { MemoryStorageOptions, StorageOptions } from "../models/domain/storageOptions";
export abstract class AbstractStorageService {
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
@ -8,5 +8,9 @@ export abstract class AbstractStorageService {
}
export abstract class AbstractCachedStorageService extends AbstractStorageService {
abstract getBypassCache<T>(key: string, options?: StorageOptions): Promise<T>;
abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}
export interface MemoryStorageServiceInterface {
get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
}

View File

@ -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);

View File

@ -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);
});
});
});

View File

@ -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<ServerConfigResponse>) {
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>): 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<ThirdPartyServerConfigResponse>) {
this.name = response.name;
this.url = response.url;
}
static fromJSON(obj: Jsonify<ThirdPartyServerConfigData>): 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<EnvironmentServerConfigResponse>) {
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>): EnvironmentServerConfigData {
return Object.assign(new EnvironmentServerConfigData({}), obj);
}
}

View File

@ -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();
});
});
});

View File

@ -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);
});
});
});

View File

@ -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<string, EncString>({
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");
});
});
});

View File

@ -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);
});
});
});

View File

@ -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();
});
});
});

View File

@ -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<TEncrypted, TDecrypted> {
encrypted?: TEncrypted;
decrypted?: TDecrypted;
decryptedSerialized?: string;
toJSON() {
return {
encrypted: this.encrypted,
decrypted:
this.decrypted instanceof ArrayBuffer
? Utils.fromBufferToByteString(this.decrypted)
: this.decrypted,
};
}
static fromJSON<TEncrypted, TDecrypted>(
obj: Jsonify<EncryptionPair<Jsonify<TEncrypted>, Jsonify<TDecrypted>>>,
decryptedFromJson?: (decObj: Jsonify<TDecrypted> | string) => TDecrypted,
encryptedFromJson?: (encObj: Jsonify<TEncrypted>) => TEncrypted
) {
if (obj == null) {
return null;
}
const pair = new EncryptionPair<TEncrypted, TDecrypted>();
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<TEncrypted, TDecrypted> {
@ -73,19 +110,66 @@ export class AccountKeys {
>();
organizationKeys?: EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Map<string, SymmetricCryptoKey>
Record<string, SymmetricCryptoKey>
> = new EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Map<string, SymmetricCryptoKey>
Record<string, SymmetricCryptoKey>
>();
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
providerKeys?: EncryptionPair<any, Record<string, SymmetricCryptoKey>> = new EncryptionPair<
any,
Map<string, SymmetricCryptoKey>
Record<string, SymmetricCryptoKey>
>();
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
publicKey?: ArrayBuffer;
publicKeySerialized?: string;
apiKeyClientSecret?: string;
toJSON() {
return Object.assign(this as Except<AccountKeys, "publicKey">, {
publicKey: Utils.fromBufferToByteString(this.publicKey),
});
}
static fromJSON(obj: DeepJsonify<AccountKeys>): 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<string, ArrayBuffer>(
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<string, SymmetricCryptoKey> = {};
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>): 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>): AccountSettings {
if (obj == null) {
return null;
}
return Object.assign(new AccountSettings(), obj, {
environmentUrls: EnvironmentUrls.fromJSON(obj?.environmentUrls),
pinProtected: EncryptionPair.fromJSON<string, EncString>(
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>): 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>): 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),
});
}
}

View File

@ -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<string, ArrayBuffer>();
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<string, string>();
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");
});
});
});

View File

@ -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>): EnvironmentUrls {
return Object.assign(new EnvironmentUrls(), obj);
}
}

View File

@ -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();
});
});
});

View File

@ -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<State,TGlobalState,TAccount> work. It currently doesn't because Globals doesn't implement Jsonify.
static fromJSON<TGlobalState extends GlobalState, TAccount extends Account>(
obj: any
): State<TGlobalState, TAccount> {
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<Account> }>
) {
if (!jsonAccounts) {
return {};
}
const accounts: { [userId: string]: Account } = {};
for (const userId in jsonAccounts) {
accounts[userId] = Account.fromJSON(jsonAccounts[userId]);
}
return accounts;
}
}

View File

@ -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<T> = StorageOptions & { deserializer?: (obj: Jsonify<T>) => T };

View File

@ -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);
}

View File

@ -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<ArrayBuffer> {

View File

@ -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<string, any>();
get<T>(key: string): Promise<T> {

View File

@ -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<TGlobalState, TAccount>,
@ -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<SymmetricCryptoKey> {
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<void> {
@ -604,23 +609,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
}
async getDecodedToken(options?: StorageOptions): Promise<any> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.tokens?.decodedToken;
}
async setDecodedToken(value: any, options?: StorageOptions): Promise<void> {
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<CipherView[]> {
return (
@ -657,11 +645,11 @@ export class StateService<
);
}
@withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON)
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
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<Map<string, SymmetricCryptoKey>> {
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<EncString> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@ -762,14 +748,9 @@ export class StateService<
}
async getDecryptedPrivateKey(options?: StorageOptions): Promise<ArrayBuffer> {
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<void> {
@ -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<Map<string, SymmetricCryptoKey>> {
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<EnvironmentUrls> {
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<void> {
@ -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<State<TGlobalState, TAccount>> {
return this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state);
protected async state(): Promise<State<TGlobalState, TAccount>> {
const state = await this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state, {
deserializer: (s) => State.fromJSON(s),
});
return state;
}
private async setState(state: State<TGlobalState, TAccount>): Promise<void> {
@ -2761,6 +2737,14 @@ export class StateService<
await this.setState(updatedState);
});
}
private mapToRecord<V>(map: Map<string, V>): Record<string, V> {
return map == null ? null : Object.fromEntries(map);
}
private recordToMap<V>(record: Record<string, V>): Map<string, V> {
return record == null ? null : new Map(Object.entries(record));
}
}
export function withPrototype<T>(
@ -2893,52 +2877,3 @@ function withPrototypeForObjectValues<T>(
};
};
}
function withPrototypeForMap<T>(
valuesConstructor: new (...args: any[]) => T,
valuesConverter: (input: any) => T = (i) => i
): (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => { value: (...args: any[]) => Promise<Map<string, T>> } {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
return {
value: function (...args: any[]) {
const originalResult: Promise<any> = 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<string, T>(Object.entries(result));
}
});
},
};
};
}

View File

@ -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<any>(v1Keys.passwordGenerationOptions)) ??
defaultAccount.settings.passwordGenerationOptions,
pinProtected: {
pinProtected: Object.assign(new EncryptionPair<string, EncString>(), {
decrypted: null,
encrypted: await this.get<string>(v1Keys.pinProtected),
},
}),
protectedPin: await this.get<string>(v1Keys.protectedPin),
settings:
userId == null

View File

@ -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<any> {
const storedToken = await this.stateService.getDecodedToken();
if (token === null && storedToken != null) {
return storedToken;
}
token = token ?? (await this.stateService.getAccessToken());
if (token == null) {

View File

@ -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<T> =
// 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<T, NotJsonable | bigint>] 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<any, any> | Set<any> ? Record<string, unknown> // {}
: T extends TypedArray ? Record<string, number>
: T extends Array<infer U>
? Array<DeepJsonify<U extends NotJsonable ? null : U>> // 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<Required<J>[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<Required<T>[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